1.背景
在日常的软件测试过程中,需要进行APP或自动化相关的日志监控(如APP测试,开发将客户端功能相关日志打印在APP中,
通过悬浮窗的形式显示的APP前端页面上)以进行程序正确性检查,常常出现日志过多,展示不美观,字体小,关闭APP后
日志丢失,造成测试人员不能够准确的识别日志
2.目的
本程序主要为将日志进行解耦,通过在WEB端展示的方式帮助测试人员快速识别日志,
持久化日志,增加测试人员测试效率
3.系统架构
系统语言:JAVA
为保证高性能,简单易用,快速部署,日志整体全部基于内存进行数据的读取,未使用任何消息中间件
系统特点:高性能 一致性
使用的组件:双向缓存阻塞队列 EVBUS 定时回收器
设计模式:观察者
架构说明:系统根据收集后的日志存取在队列中,当有消费者申请日志时,程序会开启一个对应的观察者,后续收集到的日志会
同步给此观察者,由观察者决定日志是否符合条件,当符合条件后,会将日志信息保存在观察者自有的队列中等待消费者消费,
如果观察者在指定的可配置的时间内没有进行消费,则通过定时回收器将观察者删除
性能参数(简要说明):系统进行24小时压测,在每秒1千次的日志收集和10个消费者同时消费的情况下,内存、CPU等常见性能
指标稳定,无内存泄漏、溢出,cpu过高等常见性能指标问题且响应速度在1-2毫秒范围内
4.接口
本系统共分为3个接口,分别为:日志收集接口 申请日志接口 获取日志内容接口
日志收集接口:接口收集客户端发送的内容(客户端内容必须添加必要的指定参数用于筛选过滤使用)同步到队列中
申请日志接口:接口用于消费者在指定筛选条件后返回给消费者唯一标识键,用于日志获取使用
获取日志接口:消费者将唯一标识键传入接口,接口会将指定符合条件的内容发送给消费者,这里的数据是保证一致性,
不会出现日志丢失和日志顺序错乱问题
5.源码
以下内容为核心源码,如果需要全部项目源码,请联系博主索要GitHub地址链接
控制层
package com.alibaba.logreal.controller;
import com.alibaba.logreal.common.result.HttpCodeEnum;
import com.alibaba.logreal.common.result.Result;
import com.alibaba.logreal.controller.dto.LogKeyRequestDTO;
import com.alibaba.logreal.controller.dto.LogRequestDTO;
import com.alibaba.logreal.service.LogService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.zly.utils.result.BaseResult;
import org.zly.utils.result.BaseResultFactoryImpl;
import java.util.List;
/**
* @author zly
* @version 1.0
* @date 2021/4/10 11:43
*/
@RestController
public class LogController {
@Autowired
private LogService logService;
@PostMapping(value = "/uploadLog", produces = "application/json;charset=utf-8")
public BaseResult<Object> uploadLog(@Validated LogRequestDTO logRequestDTO) {
return logService.logHandler(logRequestDTO);
}
/**
* 获取key
* 给将要获取的内容添加一个key,然后通过key请求具体的内容
*
* @param baseRequestDTO
* @return
*/
@RequestMapping(value = "/getKey", produces = "application/json;charset=utf-8")
public BaseResult<Object> getKey(LogKeyRequestDTO baseRequestDTO) {
return logService.getKeyHandler(baseRequestDTO);
}
@RequestMapping(value = "/getContent", produces = "application/json;charset=utf-8")
public BaseResult<List<String>> getContent(String key) {
if (StringUtils.isEmpty(key)) return new BaseResultFactoryImpl<List<String>>().createErrorParameter(key);
return logService.getContentHandler(key);
}
}
服务层
package com.alibaba.logreal.service.impl;
import com.alibaba.logreal.common.result.HttpCodeEnum;
import com.alibaba.logreal.common.result.Result;
import com.alibaba.logreal.common.utils.RandomUtils;
import com.alibaba.logreal.controller.dto.LogKeyRequestDTO;
import com.alibaba.logreal.controller.dto.LogRequestDTO;
import com.alibaba.logreal.event.LogEvent;
import com.alibaba.logreal.service.LogService;
import com.google.common.eventbus.EventBus;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.zly.utils.result.BaseResult;
import org.zly.utils.result.BaseResultFactoryImpl;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author zly
* @version 1.0
* @date 2021/4/10 21:57
*/
@Service
@Slf4j
@SuppressWarnings("all")
public class LogServiceImpl implements LogService {
@Autowired
private Map<String, LogEvent> logsMap;
// 观察者设计模式
@Autowired
private EventBus eventBus;
@Autowired
private BlockingDeque<LogRequestDTO> queue;
@Autowired
private ThreadPoolExecutor threadPool;
@Autowired
private ApplicationContext applicationContext;
@PostConstruct
public void init() {
// 初始化一个线程,循环的将上传的日志通知观察者
// 如果直接在接口里添加,eventBus通知唤起等待线程时是不公平的
// 日志必须要保证顺序性,所以采用通过队列的方式
this.threadPool.execute(new Runnable() {
@SneakyThrows
@Override
public void run() {
while (true) {
try {
LogRequestDTO logRequestDTO = queue.poll(60 * 1000, TimeUnit.MILLISECONDS);
if (logRequestDTO != null) {
eventBus.post(logRequestDTO);
}
} catch (Exception e) {
log.error("日志加入缓冲队列失败", e);
}
}
}
});
}
@Override
public BaseResult<Object> logHandler(LogRequestDTO logRequestDTO) {
this.queue.offer(logRequestDTO);
return new BaseResultFactoryImpl<>().createSuccess(null);
}
private final Object lock = new Object();
@Override
public BaseResult<Object> getKeyHandler(LogKeyRequestDTO baseRequestDTO) {
String key = RandomUtils.getUUID();
Queue<String> queue = (Queue<String>) applicationContext.getBean("observerQueue");
LogEvent logEvent = new LogEvent(key, baseRequestDTO, queue, System.currentTimeMillis());
this.logsMap.put(key, logEvent);
this.eventBus.register(logEvent);
return new BaseResultFactoryImpl<>().createSuccess(key);
}
@Override
public BaseResult<List<String>> getContentHandler(String key) {
LogEvent logEvent = this.logsMap.get(key);
if (logEvent != null) {
Queue<String> queue = logEvent.getQueue();
if (queue != null) {
logEvent.setSurvivalTime(System.currentTimeMillis());
List<String> list = new ArrayList<>();
while (queue.size() > 0) {
list.add(queue.poll());
}
return new BaseResultFactoryImpl<List<String>>().createSuccess(list);
}
}
return new BaseResultFactoryImpl<List<String>>().createErrorCommon("资源不存在");
}
}
日志观察者
package com.alibaba.logreal.event;
import com.alibaba.logreal.common.utils.ReflectUtils;
import com.alibaba.logreal.controller.dto.BaseRequestDTO;
import com.alibaba.logreal.controller.dto.LogKeyRequestDTO;
import com.alibaba.logreal.controller.dto.LogRequestDTO;
import com.google.common.eventbus.Subscribe;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Queue;
/**
* 日志观察者事件
*
* @author zly
* @version 1.0
* @date 2021/4/17 11:06
*/
public class LogEvent {
// 事件标识符
private String key;
// 存储事件内容的队列
private Queue<String> queue;
// 条件
private LogKeyRequestDTO baseRequestDTO;
// 存活时间
private long survivalTime;
// 条件匹配表
private static final List<Method> CONDITION_BASE;
// 使用享元模式预先缓存,较少反射带来的性能消耗
static {
CONDITION_BASE = ReflectUtils.getMemberPublicMethods(BaseRequestDTO.class);
}
public LogEvent(String key, LogKeyRequestDTO baseRequestDTO, Queue<String> queue, long survivalTime) {
this.key = key;
this.queue = queue;
this.baseRequestDTO = baseRequestDTO;
this.survivalTime = survivalTime;
}
@Subscribe
public void observer(LogRequestDTO logRequestDTO) throws InvocationTargetException, IllegalAccessException {
if (baseRequestDTO.getAll() || conditionBase(logRequestDTO)) {
this.queue.offer(logRequestDTO.getData());
}
}
/**
* 匹配条件,判断基础的条件与目标中的条件值是否先沟通
*
* @param target 目标
* @return 相同返回true
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
public boolean conditionBase(BaseRequestDTO target) throws InvocationTargetException, IllegalAccessException {
Object o;
for (Method method : CONDITION_BASE) {
o = method.invoke(this.baseRequestDTO);
if (o != null && !o.equals(method.invoke(target))){
return false;
}
}
return true;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Queue<String> getQueue() {
return queue;
}
public void setQueue(Queue<String> queue) {
this.queue = queue;
}
public LogKeyRequestDTO getBaseRequestDTO() {
return baseRequestDTO;
}
public void setBaseRequestDTO(LogKeyRequestDTO baseRequestDTO) {
this.baseRequestDTO = baseRequestDTO;
}
public long getSurvivalTime() {
return survivalTime;
}
public void setSurvivalTime(long survivalTime) {
this.survivalTime = survivalTime;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LogEvent logEvent = (LogEvent) o;
return key != null ? key.equals(logEvent.key) : logEvent.key == null;
}
@Override
public int hashCode() {
return key != null ? key.hashCode() : 0;
}
}
定时回收器
package com.alibaba.logreal.common.scheduling;
import com.alibaba.logreal.common.config.LogEventConfig;
import com.alibaba.logreal.event.LogEvent;
import com.google.common.eventbus.EventBus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Iterator;
import java.util.Map;
/**
* @author zly
* @version 1.0
* @date 2021/4/17 22:53
*/
@Component
@Slf4j
public class LogSchedul {
@Autowired
private EventBus eventBus;
@Autowired
private Map<String, LogEvent> logsMap;
@Autowired
private LogEventConfig logEventConfig;
@Scheduled(cron = "0/20 * * * * ? ")
public void removeLogObserver() {
log.info("开始清除过期的观察者,logsMap大小:" + logsMap.size());
final Iterator<Map.Entry<String, LogEvent>> iterator = logsMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, LogEvent> map = iterator.next();
LogEvent logEvent = map.getValue();
if ((System.currentTimeMillis() - logEvent.getSurvivalTime()) >= this.logEventConfig.getSurvivalTime()) {
this.eventBus.unregister(logEvent);
iterator.remove();
// log.info("清除:" + logEvent.getKey());
}
}
log.info("清除完成,logsMap大小:" + logsMap.size());
}
}
展现层:
可以使用WEB的形式展现在前端(作者实现方式:见以下截图,通过先获取key,然后通过JS定时器每xx
秒获取一次日志显示在前端),具体实现方式和展示方式由读者自定
说明:持久化可根据项目需要自行开发,可以通过接入MQ的方式将内容进行持久化操作