软件测试之-基于JAVA的高性能日志服务

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的方式将内容进行持久化操作

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值