關(guān)于后臺行為記錄,可理解為對用戶請求的監(jiān)控日志。也可理解為埋點的一種。通常埋點可簡單分為前端埋點和后端埋點。
前端埋點需要對APP或Web的一些控件進行有代碼或無代碼的監(jiān)控。然后把相應的日志信息發(fā)送到后臺。這里不展開。
服務器端埋點就相對容易,只需要攔截相應的請求,提取出關(guān)鍵信息,就能記錄一些用戶的日常行為,比如,通常該用戶的登錄、退出時間,哪些關(guān)鍵字被搜索更頻繁等等。
這里我主要介紹一下幾個地方:
- 請求的攔截;
- 將行為日志轉(zhuǎn)儲到kafka隊列;
- 將日志提取出來存到數(shù)據(jù)庫里。
關(guān)于請求的攔截,不同的java web框架的方式不太一樣。這里貼一個spring mvc的示例。首先創(chuàng)建一個請求攔截類。
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的時候已經(jīng)在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請求的內(nèi)容提取出來,然后發(fā)送到kafka隊列。我采用kafka里String的序列化方式。為了最后實例化成對象更容易,可以先把對象轉(zhuǎn)化成json, 然后傳輸json string。
現(xiàn)在最關(guān)鍵的就是讓這個攔截器生效。我這邊是在spring-mvc.xml里面配置。通過這個方式就可以把TrackInterceptor類注冊到攔截器中。
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.test.interceptor.TrackInterceptor"></bean>
</mvc:interceptor>
現(xiàn)在已經(jīng)完成了兩個步驟,接下來就是從kafka隊列里讀取相應的內(nèi)容。我這里讀取出來直接解析到數(shù)據(jù)庫里。
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的好處,可以增加代碼的可移植性。
最后我們?nèi)タ纯磾?shù)據(jù)庫里的日志:
這個就是后臺行為日志跟蹤的大致過程。我將kafka的接口和日志提取就放到了github上。大家可以下載使用。只有請求攔截TrackInterceptor這個類需要自己寫。代碼是基于jfinal框架實現(xiàn)的,僅供參考。重要是解決思路上的問題。