微服务日志
前言
最近接到一个任务需要实现微服务下的日志功能,因为和单服务不同,根据自己的经验和网上查找到的资料,我想到了3种方式来实现,第一个就是ELK微服务实现日志框架,网上基本也是用这个做的。
关于ELK的实现在下一章进行实现。
ELK框架虽然很强大,各种可视化界面非常完善,但是做日志我们任然习惯想单体服务那样来实现,应为我想到2种解决方案。
1.通过公共模块设置切面,再利用微服务的特性,使用feign调用日志模块的接口保存日志。
2.通过公共模块设置切面,再通过消息队列保存日志信息,日志模块接收消息,异步处理。
优缺点: 虽然feign这个方法能达到同步的效果,但是对于日志功能来说,对同步的需求要求低,并且微服务下模块多,记录的日志多,访问大,使用feign调用会占用较多的资源,导致服务器卡顿,因为我选用消息队列的方式来异步实现。
一、消息列表和日志模块
1.消息列队和微服务
首先写这个功能前需要了解微服务和消息队列的一些基本知识,在这里我用的是rabbitMq和springcloud alibaba
2.公共模块设置切面
创建公共模块,并在日志模块引入依赖,我使用的是注解做切面,核心切面类,代码如下:
package com.kbplus.demo.common.config;
import com.kbplus.demo.common.annotation.SystemLog;
import com.kbplus.demo.common.entity.MyLog;
import com.kbplus.demo.common.sender.ModuleRequestSender;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
/**
* @author kbplus
* @date 2022-03-28
* @blog https://blog.csdn.net/cyy9487
*/
@Order(1)
@Component
@Aspect
public class LogAspect {
@Autowired
private ModuleRequestSender moduleRequestSender;
/**
* 定义切点
*/
@Pointcut("@annotation(com.kbplus.demo.common.annotation.SystemLog)")
public void doAspect() {
}
@Around("doAspect()")
public Object doAround(ProceedingJoinPoint pjd) throws Throwable {
ServletRequestAttributes attributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes==null){
return null;
}
HttpServletRequest request = attributes.getRequest();
//获取方法注释
String operation="";
String logType="";
Signature signature=pjd.getSignature();
if (signature instanceof MethodSignature) {
Class<?> targetCls=pjd.getTarget().getClass();
MethodSignature ms= (MethodSignature)signature;
Method targetMethod=targetCls.getDeclaredMethod(ms.getName(),ms.getParameterTypes());
SystemLog requiredLog=targetMethod.getAnnotation(SystemLog.class);
if(requiredLog!=null) {
operation = requiredLog.description();
logType = requiredLog.type().getName();
}
}
List<String> params = new ArrayList<>();
//获取参数
Enumeration<String> enu = request.getParameterNames();
if(enu.hasMoreElements()) {
while (enu.hasMoreElements()) {
String paraName = enu.nextElement();
params.add(paraName + ": " + request.getParameter(paraName));
}
}else {
Object[] args = pjd.getArgs();
for (Object arg : args) {
params.add(arg.toString());
}
}
MyLog myLog = new MyLog();
myLog.setId(UUID.randomUUID().toString());
myLog.setMethodType(request.getMethod());
myLog.setType(logType);
myLog.setParams(params);
myLog.setModuleName(request.getContextPath().substring(1));
myLog.setUrl(request.getRequestURI());
myLog.setIp(request.getRemoteAddr());
myLog.setComment(operation);
long startTime = System.currentTimeMillis();
Object proceed = pjd.proceed();
myLog.setCost(System.currentTimeMillis()-startTime);
moduleRequestSender.send(myLog);
return proceed;
}
}
切面会需要的坑(切面不生效):
启动类加上包扫描
@SpringBootApplication(scanBasePackages = {"com.kbplus.demo.*"})
二、日志模块处理
创建日志模块,并添加dao-service-controller,集成elasticsearch保存和查询日志,也可以使用mongodb
作为替代。
elasticsearch分页查询核心类
package com.kbplus.demo.log.service.impl;
import com.kbplus.demo.common.entity.Page;
import com.kbplus.demo.common.entity.PageRequest;
import com.kbplus.demo.log.dao.LogDao;
import com.kbplus.demo.log.model.entity.MyLog;
import com.kbplus.demo.log.model.query.LogQuery;
import com.kbplus.demo.log.service.LogService;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* @author kbplus
* @date 2022-03-28
* @blog https://blog.csdn.net/cyy9487
*/
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao dao;
@Override
public void save(MyLog myLog) {
myLog.setRecordTime(new Date());
dao.save(myLog);
}
@Override
public Page<MyLog> getAllPage(LogQuery logQuery, PageRequest pageRequest){
Pageable pageable = org.springframework.data.domain.PageRequest.of(pageRequest.getPage()-1,pageRequest.getSize(), Sort.by(Sort.Direction.DESC,"recordTime"));
BoolQueryBuilder queryBuilder= QueryBuilders.boolQuery();
MatchQueryBuilder matchQueryBuilder1;
MatchQueryBuilder matchQueryBuilder2;
if(StringUtils.isNotEmpty(logQuery.getModuleName())) {
matchQueryBuilder1 = new MatchQueryBuilder("moduleName", logQuery.getModuleName());
queryBuilder.must(matchQueryBuilder1);
}
if(StringUtils.isNotEmpty(logQuery.getType())) {
matchQueryBuilder2 = new MatchQueryBuilder("type", logQuery.getType());
queryBuilder.must(matchQueryBuilder2);
}
org.springframework.data.domain.Page<MyLog> search = dao.search(queryBuilder,pageable);
Page<MyLog> page = new Page<>(pageRequest.getPage(),pageRequest.getSize());
page.setTotal(search.getTotalElements());
page.setCurrent(search.getNumber()+1);
page.setRecords(search.getContent());
return page;
}
}
MQ配置
2个配置,一个在公共模块,一个在日志模块
package com.kbplus.demo.common.config;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
/**
* @author kbplus
* @date 2022-03-28
* @blog https://blog.csdn.net/cyy9487
*/
@Configuration
public class DirectRabbitConfig implements RabbitListenerConfigurer {
@Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
return rabbitTemplate;
}
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
}
@Bean
MessageHandlerMethodFactory messageHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory messageHandlerMethodFactory = new DefaultMessageHandlerMethodFactory();
messageHandlerMethodFactory.setMessageConverter(consumerJackson2MessageConverter());
return messageHandlerMethodFactory;
}
@Bean
public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
return new MappingJackson2MessageConverter();
}
}
日志模块:
package com.kbplus.demo.log.common.configuration;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
/**
* @author kbplus
* @date 2022-03-28
* @blog https://blog.csdn.net/cyy9487
*/
@Configuration
public class LogDirectRabbitConfig {
/**
* 日志队列
*/
@Bean
public Queue createModuleRequestQueue() {
return new Queue("log",true);
}
@Bean
public DirectExchange createModuleRequestExchange() { return new DirectExchange("log_exchange",true,false); }
@Bean
public Binding ModuleRequestBindingDirect() {
return BindingBuilder.bind(createModuleRequestQueue()).
to(createModuleRequestExchange()).
with("log_request_key");
}
}
生产在切面类,切指定注解切面并生产消息
生产者-日志模块:
package com.kbplus.demo.log.recevier;
import com.kbplus.demo.log.model.entity.MyLog;
import com.kbplus.demo.log.service.LogService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
/**
* @author kbplus
* @date 2022-03-28
* @blog https://blog.csdn.net/cyy9487
*/
@Slf4j
@Component
public class ModuleReceiver {
@Autowired
private LogService logService;
/**
* 日志收集处理
*
*/
@RabbitListener(queues = "log")
@Transactional(rollbackFor = Exception.class)
public void connectBase(MyLog myLog, Channel channel, Message message) throws IOException {
try {
logService.save(myLog);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
总结
以上就是今天要讲的内容。代码详细源码:微服务日志功能的实现rabbitmq+elasticsearch+aop