在springcloud分布式微服务中,每个微服务都要配置一个日志输出文件,当微服务多起来的时候,日志输出有变动就要一个一个微服务去修改,这样使工作量增加,变得很麻烦,还有可能出现错误。

对日志文件进行统一的配置处理是个不错的选择。

首先在微服务中有一个基础的模块是存放一些基础的,共用的工具,配置,common模块,所有项目都依赖common模块。

微服务 日志 收集 微服务日志统一处理_微服务 日志 收集

 首先在服务中appliction-dev.yml中进行配置:

微服务 日志 收集 微服务日志统一处理_微服务_02

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;
    }
}