掘金 后端 ( ) • 2024-04-15 17:39

日志处理

上线项目最怕的就是出问题没有办法定位,日志基本上是第一排查方向,所有开发之初,设计好日志分类和展示是非常关键的

方案选择

输出到日志文件中,log4j

pom依赖

       <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

项目中写log4j.properties

log4j.rootLogger=TRACE, stdout, R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r %-5p  %d{yyyy-MM-dd HH:mm:ssS} %c -%m%n
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=example.log
log4j.appender.R.MaxFileSize=100KB
log4j.appender.R.MaxBackupIndex=1
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=[%p] %t %d{yyyy-MM-dd HH:mm:ss,SSS} %c - {%m} %n 
log4j.logger.com.foo=WARN

使用

private Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);

logger.info("开启线程池");

一般一个单体项目日志的输出打印就可以支撑了,但是查看日志只能去服务器下载查看,还有日志文件过大需要按时清理

输出到库里

我们目前选择建立一个日志服务,进行日志入库,方便线上直接查看日志 具体方案如下:

image.png

先完成基础的日志分类,分别分为通用的日志、接口访问日志、错误日志三种,提供feign接口给其他服务使用,也可以提供消费者给其他服务发送消息,完成消息入库

@RestController
@AllArgsConstructor
public class LogClient implements ILogClient {

   private final ILogUsualService usualLogService;

   private final ILogApiService apiLogService;

   private final ILogErrorService errorLogService;

   @Override
   @PostMapping(API_PREFIX + "/saveUsualLog")
   public R<Boolean> saveUsualLog(@RequestBody LogUsual log) {
      log.setParams(log.getParams().replace("&amp;", "&"));
      return R.data(usualLogService.save(log));
   }

   @Override
   @PostMapping(API_PREFIX + "/saveApiLog")
   public R<Boolean> saveApiLog(@RequestBody LogApi log) {
      log.setParams(log.getParams().replace("&amp;", "&"));
      return R.data(apiLogService.save(log));
   }

   @Override
   @PostMapping(API_PREFIX + "/saveErrorLog")
   public R<Boolean> saveErrorLog(@RequestBody LogError log) {
      log.setParams(log.getParams().replace("&amp;", "&"));
      return R.data(errorLogService.save(log));
   }
}
  • 接口日志:aop + 自定义注解实现日志入库

自定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {
    String value() default "日志记录";
}

aop

@Aspect
public class ApiLogAspect {
    private static final Logger log = LoggerFactory.getLogger(ApiLogAspect.class);

    public ApiLogAspect() {
    }

    @Around("@annotation(apiLog)")
    public Object around(ProceedingJoinPoint point, ApiLog apiLog) throws Throwable {
        String className = point.getTarget().getClass().getName();
        String methodName = point.getSignature().getName();
        long beginTime = System.currentTimeMillis();
        Object result = point.proceed();
        long time = System.currentTimeMillis() - beginTime;
        ApiLogPublisher.publishEvent(methodName, className, apiLog, time);
        return result;
    }
}

事件

public class ApiLogPublisher {
    public ApiLogPublisher() {
    }

    public static void publishEvent(String methodName, String methodClass, ApiLog apiLog, long time) {
        HttpServletRequest request = WebUtil.getRequest();
        LogApi logApi = new LogApi();
        logApi.setType("1");
        logApi.setTitle(apiLog.value());
        logApi.setTime(String.valueOf(time));
        logApi.setMethodClass(methodClass);
        logApi.setMethodName(methodName);
        LogAbstractUtil.addRequestInfoToLog(request, logApi);
        Map<String, Object> event = new HashMap(16);
        event.put("log", logApi);
        SpringUtil.publishEvent(new ApiLogEvent(event));
    }
}

SpringUtil.publishEvent(new ApiLogEvent(event));

这里用到的事件后面有空另外研究

  • 错误日志:异常拦截器 + 监听错误日志进库

使用@RestControllerAdvice注解进行异常拦截

import javax.servlet.Servlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.DispatcherServlet;

@Order
@AutoConfiguration
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@RestControllerAdvice
public class BladeRestExceptionTranslator {
    private static final Logger log = LoggerFactory.getLogger(BladeRestExceptionTranslator.class);
    private final BladeRequestLogProperties properties;

    @ExceptionHandler({ServiceException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public R handleError(ServiceException e) {
        log.error("业务异常", e);
        return R.fail(e.getResultCode(), e.getMessage());
    }

    @ExceptionHandler({SecureException.class})
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public R handleError(SecureException e) {
        log.error("认证异常", e);
        return R.fail(e.getResultCode(), e.getMessage());
    }

    @ExceptionHandler({Throwable.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public R handleError(Throwable e) {
        log.error("服务器异常", e);
        if (this.properties.getErrorLog()) {
            ErrorLogPublisher.publishEvent(e, UrlUtil.getPath(WebUtil.getRequest().getRequestURI()));
        }

        return R.fail(ResultCode.INTERNAL_SERVER_ERROR, Func.isEmpty(e.getMessage()) ? ResultCode.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage());
    }

    public BladeRestExceptionTranslator(final BladeRequestLogProperties properties) {
        this.properties = properties;
    }
}

然后可以使用SpringUtil.publishEvent(new ErrorLogEvent(event));事件,或者集成为消息队列来实现这部分的日志入库

  • 业务日志:feign调用或者mq入库

正常使用feign接口或者调用生产者发送消息即可

最后日志清理这方面,由业务设计来完成,启用定时任务,清理三月之前的日志信息

输出到中间件

elk后续使用时再丰富