文档章节

APDPlat中业务日志和监控日志的设计与实现

杨尚川
 杨尚川
发布于 2014/01/29 01:37
字数 2140
阅读 332
收藏 4

APDPlat提供了业务日志监控日志,以便对用户操作进行审计、对系统性能进行调优。

 

业务日志主要包括数据的增删改日志、备份恢复日志以及用户登录注销日志。监控日志主要包括用户请求响应时间、内存使用情况、全文索引重建情况、系统启动关闭事件。

 

设计目标:

 

1、灵活,可以很容易地启用或停用

2、性能,不对正常的业务操作造成影响

3、开放,容易和第三方系统整合

 

下面阐述具体的设计及实现:

 

1、在灵活性方面,可以在配置文件config.properties或config.local.properties中指定选项来启用(true)或停用(false),如下所示:

 

   

    配置:

#是否启用系统监控模块中的功能
monitor.memory=true
monitor.performance=true
monitor.runing=true
monitor.login=true
monitor.index=true
monitor.backup=true
#是否启用业务日志记录功能
log.create=true
log.delete=true
log.update=true

   

    代码:

memoryMonitor=PropertyHolder.getBooleanProperty("monitor.memory");        
if(memoryMonitor){
	LOG.info("启用内存监视日志");
	LOG.info("Enable memory monitor log", Locale.ENGLISH);
}else{
	LOG.info("禁用内存监视日志");
	LOG.info("Disable memory monitor log", Locale.ENGLISH);
}

  

2、在性能方面,使用内存缓冲区来临时存储日志对象,可以节省磁盘和网络开销,缓冲区的大小可以配置,当缓冲区满了或是人工强制执行的时候才会对日志进行持久化或其他处理,这样不但提高了吞吐量(批量提交、批量处理),而且对用户业务处理的影响非常小,因为产生日志对象之后只需要将日志对象加入缓冲区即可(无阻塞、内存操作)。除此之外,当对缓冲区中的日志对象进行持久化或其他处理的时候,会有独立的线程池中的线程来完成,不会阻塞用户业务处理线程(线程复用、异步非阻塞),如下所示:

 

MemoryState logger=new MemoryState();
try {
 logger.setServerIP(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException ex) {
 LOG.error("获取服务器地址出错",ex);
 LOG.error("Can't get server's internet address", ex, Locale.ENGLISH);
}
logger.setAppName(SystemListener.getContextPath());
logger.setRecordTime(new Date());
logger.setMaxMemory(max);
logger.setTotalMemory(total);
logger.setFreeMemory(free);
logger.setUsableMemory(logger.getMaxMemory()-logger.getTotalMemory()+logger.getFreeMemory());
BufferLogCollector.collect(logger);

 

    首先,构造了一个日志对象logger,设置相关信息,然后调用BufferLogCollector.collect(logger)将日志对象加入内存缓冲区,BufferLogCollector.collect方法如下:

 

public static <T extends Model> void collect(T t){
	LOG.debug("将日志加入缓冲区:\n"+t.toString());
	buffers.add(t);
	//判断缓冲区是否达到限制
	if(buffers.size() > logBufferMax){
		LOG.info("缓冲区已达到限制数:"+logBufferMax+" ,处理日志");
		handleLog();
	}
}

 

    buffers是类ConcurrentLinkedQueue的实例,不限制大小,不会有日志存不下而暂停阻塞的情况发生,支持多线程并发操作,链表结构,尤其适合增删操作。加入缓冲区之后就会判断缓冲区是否已满,如满则会处理,logBufferMax的值从哪里来的呢?

 

private static final int logBufferMax = PropertyHolder.getIntProperty("log.buffer.max");

 

     log.buffer.max的值需要在配置文件config.properties或config.local.properties中指定,值越大,吞吐量越好,对用户的影响越小(除了批量处理的时候,发生次数很少),当然内存的占用也越大,需要根据实际情况权衡:

 

log.buffer.max=1000

 

    如果缓冲区满了,怎么处理日志呢?看看handleLog方法:

 

public static void handleLog(){
	if(shoudHandle()){
		executorService.submit(handleLogRunnable);
	}
}

 

     先判断是否应该处理,看是否有logHandlers,对缓冲区大小做了检查,如下所示:

 

private static boolean shoudHandle(){
	if(logHandlers.isEmpty()){    
		LOG.error("未找到任何LogHandler");
		return false;
	}
	int len=buffers.size();
	if(len==0){
		LOG.info("没有日志需要保存");
		LOG.info("No logs need to save:"+len, Locale.ENGLISH);
		return false;
	}
	return true;
}

 

    这里使用了ExecutorService来对线程进行管理,如检查通过,则将具体的日志处理逻辑(封装在handleLogRunnable中)提交给executorService 

 

private static final ExecutorService executorService = Executors.newSingleThreadExecutor();

 

    handleLog方法把具体的日志处理逻辑(封装在handleLogRunnable中)提交给executorService (线程执行服务),对线程的提交和执行做了解耦,复用线程池,提高了性能。由于具体的日志处理逻辑运行于独立的线程中,故不会阻塞用户业务处理线程。

 

3、怎么理解开放呢?先接着上面看看提交给线程执行服务的日志的处理逻辑:

 

private static final HandleLogRunnable handleLogRunnable = new HandleLogRunnable();

 

    在线程中调用了私有静态内部类LogSaver的save方法,如下所示:

 

private static class HandleLogRunnable implements Runnable{
	@Override
	public void run() {
		LOG_SAVER.save();
	}    
}

 

private static final LogSaver LOG_SAVER = new LogSaver();

 

    save里面究竟是怎么处理的呢?请看:

 

private static class LogSaver{
	public void save(){
		int len=buffers.size();
		List<Model> list=new ArrayList<>(len);
		for(int i=0;i<len;i++){
			list.add(buffers.remove());            
		}        
		//把日志交给LogHandler处理
		for(LogHandler logHandler : logHandlers){
			logHandler.handle(list);
		}            
	}
}

 

    首先把缓冲区里面的数据全部取出来(取出来之后缓冲区里面就没有了)构成一个链表,数目不超过规定的大小(配置文件中指定的log.buffer.max的值),然后把链表分别交给每一个注册的LogHandler来处理。注意这里不用多个线程来让多个LogHandler并行执行的原因:一是会增大系统负荷,对用户业务处理造成影响;二是多个LogHandler并行执行的话就满足不了需要先后执行顺序的要求了。

 

    接着看LogHandler,这是一个接口:

 

/**
 * 日志处理接口:
 * 可将日志存入独立日志数据库(非业务数据库)
 * 可将日志传递到activemq\rabbitmq\zeromq等消息队列
 * 可将日志传递到kafka\flume\chukwa\scribe等日志聚合系统
 * @author 杨尚川
 */
public interface LogHandler {
    public <T extends Model> void handle(List<T> list);
}

 

    那么logHandlers是怎么来的呢?

 

private static final List<LogHandler> logHandlers = new ArrayList<>();

 

@Service
public class BufferLogCollector  implements ApplicationListener {

 

    BufferLogCollector实现了Spring的ApplicationListener接口,当Spring的所有对象正确完整地装配完成后会回调BufferLogCollector实现的方法:

 

@Override
public void onApplicationEvent(ApplicationEvent event){
	if(event instanceof ContextRefreshedEvent){
		LOG.info("spring容器初始化完成,开始解析LogHandler");
		String handlerstr = PropertyHolder.getProperty("log.handlers");
		if(StringUtils.isBlank(handlerstr)){
			LOG.info("未配置log.handlers");
			return;
		}
		LOG.info("handlerstr:"+handlerstr);
		String[] handlers = handlerstr.trim().split(";");
		for(String handler : handlers){
			LogHandler logHandler = SpringContextUtils.getBean(handler.trim());
			if(logHandler != null){
				logHandlers.add(logHandler);
				LOG.info("找到LogHandler:"+handler);
			}else{
				LOG.info("未找到LogHandler:"+handler);
			}
		}
	}
}

 

    怎么跟Spring扯上关系了呢?因为logHandlers是从Spring的容器中获得的。从这里可以得知,logHandlers是由配置文件config.properties或config.local.properties中的log.handlers选项指定的,如下所示:

 

#日志缓冲区的最大值,只有达到最大值或手工强制刷新时,日志才会被持久化
#当用户在管理界面查看任意一种日志时,会强制刷新
#log.handlers可指定多个,用;分割,值为spring的bean名称
#如:databaseLogHandler;fileLogHandler;consoleLogHandler
log.buffer.max=1000
log.handlers=databaseLogHandler;

 

    开放的含义体现在:LogHandler定义了统一的接口,允许任意的扩展,LogHandler的实现由Spring来管理,通过在配置文件中指定log.handlers的值为托管在Spring中的多个bean name,可以有序地调用多个LogHandler实现,调用顺序就是配置文件中指定的先后顺序。

  

 

最后来看几个LogHandler的实现:

 

1、DatabaseLogHandler(将日志保存到关系数据库,使用JPA)

 

@Service
public class DatabaseLogHandler implements LogHandler{
    private static final APDPlatLogger LOG = new APDPlatLogger(DatabaseLogHandler.class);
    //使用日志数据库
    @Resource(name = "serviceFacadeForLog")
    private ServiceFacade serviceFacade;

    /**
     * 打开日志数据库em
     * @param entityManagerFactory 
     */
    private static void openEntityManagerForLog(EntityManagerFactory entityManagerFactory){        
        EntityManager em = entityManagerFactory.createEntityManager();
        TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(em));
        LOG.info("打开ForLog实体管理器");
    }
    /**
     * 关闭日志数据库em
     * @param entityManagerFactory 
     */
    private static void closeEntityManagerForLog(EntityManagerFactory entityManagerFactory){
        EntityManagerHolder emHolder = (EntityManagerHolder)TransactionSynchronizationManager.unbindResource(entityManagerFactory);
        LOG.info("关闭ForLog实体管理器");
        EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
    }
    @Override
    public <T extends Model> void handle(List<T> list) {
        int len = list.size();
        LOG.info("需要保存的日志数目:"+len);
        LOG.info("The number of logs to be saved:"+len, Locale.ENGLISH);
        long start=System.currentTimeMillis();
        EntityManagerFactory entityManagerFactoryForLog = SpringContextUtils.getBean("entityManagerFactoryForLog");
        openEntityManagerForLog(entityManagerFactoryForLog);
        //保存日志
        serviceFacade.create(list);
        closeEntityManagerForLog(entityManagerFactoryForLog);
        long cost=System.currentTimeMillis()-start;
        LOG.info("成功保存 "+len+" 条日志, 耗时: "+ConvertUtils.getTimeDes(cost));
        LOG.info("Success to save "+len+" logs, elapsed: "+ConvertUtils.getTimeDes(cost), Locale.ENGLISH);
    }
}

 

2、FileLogHandler(将日志保存到本地文件)

 

@Service
public class FileLogHandler implements LogHandler{
    private static int count = 1;

    @Override
    public <T extends Model> void handle(List<T> list) {
        StringBuilder str = new StringBuilder();
        for(T t : list){
            str.append(count++).append(":\n").append(t.toString());
        }
        FileUtils.createAndWriteFile("/WEB-INF/logs/log-"+DateTypeConverter.toDefaultDateTime(new Date()).replace(" ", "-").replace(":", "-")+".txt", str.toString());
    }
}

 

3、ConsoleLogHandler(将日志在控制台输出)

 

@Service
public class ConsoleLogHandler implements LogHandler{
    private static int count = 1;
    @Override
    public <T extends Model> void handle(List<T> list) {
        for(T t : list){
            System.out.println((count++) + ":");
            System.out.println(t.toString());
        }
    }
}

 

 

 

APDPlat托管在Github

© 著作权归作者所有

杨尚川

杨尚川

粉丝 1103
博文 220
码字总数 1624053
作品 12
东城
架构师
私信 提问
APDPlat拓展搜索之集成Solr

APDPlat充分利用Compass的OSEM和ORM integration特性,提供了简单易用且功能强大的内置搜索特性。 APDPlat的内置搜索,在设计简洁优雅的同时,还具备了强大的实时搜索能力,用户只需用注解的...

杨尚川
2014/02/01
662
0
APDPlat拓展搜索之集成ElasticSearch

APDPlat充分利用Compass的OSEM和ORM integration特性,提供了简单易用且功能强大的内置搜索特性。 APDPlat的内置搜索,在设计简洁优雅的同时,还具备了强大的实时搜索能力,用户只需用注解的...

杨尚川
2014/02/01
292
2
APDPlat的系统启动和关闭流程剖析

APDPlat接管了Spring的启动关闭权,为各种运行其上的开源框架和类库的无缝集成提供了支持。 当然,大家都知道,一个JAVA EE Web应用的入口点是web.xml,APDPlat当然也不例外,我们看看APDPl...

杨尚川
2014/02/03
414
0
APDPlat的日志国际化实现方式

APDPlat使用slf4j来做日志框架,由于slf4j不支持国际化日志,所以APDPlat使用自定义的APDPlatLogger类来支持国际化日志。使用方法如下: 1、构造日志对象 private static final APDPlatLogg...

杨尚川
2014/01/29
257
0
基于word分词提供的文本相似度算法来实现通用的网页相似度检测

实现代码:基于word分词提供的文本相似度算法来实现通用的网页相似度检测 运行结果: 检查的博文数:128 1、检查博文:192本软件著作用词分析(五)用词最复杂99级,相似度分值:Simple=0.96...

杨尚川
2015/05/28
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

Git 分支管理规范

Git 仓库申请流程 开发主管向 Git 管理员提交 Git 仓库申请【邮件:发送给 Git 管理员,抄送给项目经理,申请表可向 Git 管理员获取】 Git 管理员审批开发主管的申请,审批以下具体信息: 审批...

物种起源-达尔文
23分钟前
6
0
浅谈iterator迭代器模式

一、前言 设计模式有很多,最典型的是GoF的23种设计模式,听起来很多,其实大部分我们都是见过的,按照常见度来分,最常用的差不多是六七个吧,因此,我们在学习的时候应该有轻重缓急之分,不...

青衣霓裳
23分钟前
4
0
Spring Boot2 系列教程(二)创建一个 Spring Boot 项目的三种方法

我最早是 2016 年底开始写 Spring Boot 相关的博客,当时使用的版本还是 1.4.x ,文章发表在 CSDN 上,阅读量最大的一篇有 43W+,如下图: 2017 年由于种种原因,就没有再继续更新 Spring B...

江南一点雨
25分钟前
5
0
熟练掌握这5个Excel技巧,只加薪不加班

月初月报的整理,周一周报的整理都离不开Excel,有的同事只要10分钟搞定,也有同事花费一上午时间整理报表,这样对比起来,时间差是很大的。那怎样快速对Excel表中的数据进行整理呢? 1.快速...

干货趣分享
30分钟前
5
0
总结:SpringCloud

一、Eureka Eureka 是 Netflix 开发的,一个基于 REST 服务的,服务注册与发现的组件 它主要包括两个组件:Eureka Server 和 Eureka Client Eureka Client:一个Java客户端,用于简化与 Eure...

浮躁的码农
39分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部