在springcloud分布式微服务中,每个微服务都要配置一个日志输出文件,当微服务多起来的时候,日志输出有变动就要一个一个微服务去修改,这样使工作量增加,变得很麻烦,还有可能出现错误。
对日志文件进行统一的配置处理是个不错的选择。
首先在微服务中有一个基础的模块是存放一些基础的,共用的工具,配置,common模块,所有项目都依赖common模块。
首先在服务中appliction-dev.yml中进行配置:
logging:
path: /yunpan/logs/admin
config: classpath:logback-spring.xml
# logstash连接配置
host:
port:
level:
root: info
在服务中进行spring_logback.xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" packagingData="true">
<!-- log application level -->
<springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" />
<include resource="com/zj/common/logback/commonLogback.xml" />
<!-- 日志输出级别 -->
<root level="${LOG_LEVEL}">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ALL_LOG" />
<appender-ref ref="ALL_ERROR" />
<appender-ref ref="LOGSTASH" />
</root>
</configuration>
<include resource="指向公共的配置文件路径">
在commonLogback.xml 中这样配置,当然还可以添加其他的配置属性,具体配置可以另行查找。
<?xml version="1.0" encoding="UTF-8"?>
<included>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- <configuration scan="false" scanPeriod="60 seconds" debug="false"> -->
<!-- log application name -->
<springProperty scope="context" name="springAppName" source="spring.application.name" />
<springProperty scope="context" name="logLevel" source="logging.level.root" />
<!--设置系统日志目录-->
<springProperty scope="context" name="logPath" source="logging.path"/>
<!--logstash输入地址-->
<springProperty scope="context" name="LOG_HOST" source="logging.host"/>
<springProperty scope="context" name="LOG_PORT" source="logging.port"/>
<springProperty scope="context" name="env" source="spring.profiles.active" defaultValue="dev"/>
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger[%line] - %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- 记录项目所有日志 -->
<appender name="ALL_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<file>${logPath}/${springAppName}.log</file>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${logPath}/back/${springAppName}.log.%d{yyyy-MM-dd}.%i</FileNamePattern>
<MaxHistory>30</MaxHistory>
<!-- 除按日志记录之外,还配置了日志文件不能超过10M,若超过10M,日志文件会以索引0开始,
命名日志文件,例如honey.log.2017-12-29.0 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>
<!-- 记录项目ERROR级别文件 -->
<appender name="ALL_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<file>${logPath}/${springAppName}-error.log</file>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${logPath}/back/${springAppName}-error.log.%d{yyyy-MM-dd}.%i</FileNamePattern>
<MaxHistory>30</MaxHistory>
<!-- 除按日志记录之外,还配置了日志文件不能超过10M,若超过10M,日志文件会以索引0开始,
命名日志文件,例如honey-error.logger.logger-2017-12-29.0 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>
<!--配置logstash-->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_HOST:- }:${LOG_PORT:- }</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<pattern>
<pattern> {
"env": "${env:-}",
"severity": "%level",
"service": "${springAppName:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{60}",
"rest": "#tryJson{%message}"
}
</pattern>
</pattern>
</providers>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<logger name="jdbc.sqltiming" additivity="false" level="DEBUG">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ALL_LOG" />
<appender-ref ref="ALL_ERROR" />
<appender-ref ref="LOGSTASH" />
</logger>
<logger name="com.vanmilk" additivity="false" level="DEBUG">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ALL_LOG" />
<appender-ref ref="ALL_ERROR" />
<appender-ref ref="LOGSTASH" />
</logger>
<!-- 去掉spring打印,实际并没有错, Could not find default TaskScheduler bean异常 -->
<logger name="org.springframework.scheduling">
<level value="info" />
</logger>
</included>
这样在以后添加其他的微服务只需要添加logback-spring.xml就可以了,其他的都会共用commonLogback.xml中的省去了许多工作量。
最好的是能在common模块中再加一个AOP进行全局日志输出管理。
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Created by Yuyang Zhang on 2023-03-08
* <p>
* 业务耗时日志记录
*/
@Slf4j
@Aspect
public class ControllerLogAop {
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)||" +
"@annotation(org.springframework.web.bind.annotation.PostMapping)||" +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping)||" +
"@annotation(org.springframework.web.bind.annotation.PutMapping)||" +
"@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void controllerMethod() {
}
@Before("controllerMethod()")
public void doControllerBefore(final JoinPoint joinPoint) {
final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.currentRequestAttributes()).getRequest();
final String ip = IpUtils.getRemoteAddrIp(request);
final String errorCode = CommonUtils.getRandomNumberString(8);
final String params = getParams(joinPoint);
final String targetName = joinPoint.getTarget().getClass().getName();
final String methodName = targetName + "." + joinPoint.getSignature().getName();
final String url = request.getRequestURL() +
(StringUtils.isNotEmpty(request.getQueryString()) ? "?" + request.getQueryString() : "");
final StringBuilder message = new StringBuilder();
final SysAccountToken currentUser = UserContext.getCurrentUser();
String token = request.getHeader("token") == null ? "UN_KNOW" : request.getHeader("token");
String appName = request.getHeader("AppName");
String appVersion = request.getHeader("AppVersion");
try {
message.append("\r\n==============Request Start==============")
// .append("\r\n请求方法:").append(methodName)
.append("\r\n[Requested URL]: ").append(url)
.append("\r\n[Requested Params]: ").append(params)
.append("\r\n[Requested Method]: ").append(request.getMethod())
.append("\r\n[Requested Authorization]: ").append(token);
if (appName != null && appVersion != null) {
message.append("\r\n[Requested App Name]: ").append(appName)
.append("\r\n[Requested App Version]: ").append(appVersion);
}
message.append("\r\n[Requested IP]: ").append(ip)
.append("\r\n[Requested By]:").append(currentUser == null ?
"anonymous" : currentUser.getFirstName() + " " + currentUser.getLastName() + "[" + currentUser.getId() + "]")
.append("\r\n==============Request End==============");
log.info("Requested Information:{}", message.toString());
} catch (final Exception e) {
log.error("异常信息:{}", e.getMessage(), e);
throw e;
}
}
/**
* 方法执行拦截,输出请求处理的时间
*
* @param joinPoint 切入点
* @throws Throwable 异常
*/
@Around("controllerMethod()")
public Object doControllerAround(final ProceedingJoinPoint joinPoint) throws Throwable {
final String targetName = joinPoint.getTarget().getClass().getName();
final String methodName = targetName + "." + joinPoint.getSignature().getName();
final long start = System.currentTimeMillis();
final Object object = joinPoint.proceed();
final long executeTime = System.currentTimeMillis() - start;
log.info("Polaris Biz [{}] execute time {} ms", methodName, executeTime);
return object;
}
/**
* 获取请求的参数
*
* @param joinPoint 切入点
* @return 参数JSON ARRAY
*/
private String getParams(final JoinPoint joinPoint) {
final Object[] args = joinPoint.getArgs();
final List<Object> params = new ArrayList<>();
for (final Object o : args) {
if (o instanceof ServletRequest) {
final HttpServletRequest request = (HttpServletRequest) o;
params.addAll(Arrays.asList(request.getParameterMap().values().toArray()));
continue;
}
if (o instanceof ServletResponse) {
continue;
}
if (o instanceof MultipartFile) { //过滤文件上传的参数
continue;
}
params.add(o);
}
String result = null;
try {
result = JSON.toJSONString(params);
} catch (final Exception e) {
log.error("controller 参数拦截失败,{}", e.getMessage(), e);
}
return result;
}
}