springboot logback多租户根据请求打印日志到不同文件

前提:不修改之前打印日志的代码(否则工作量太大),支持并发

服务改造为多租户服务,日志打印在一起不能区分是哪个租户请求的,在不改造项目之前打印日志代码的前提下,根据请求打印日志到不同文件。

一般项目中日志打印:

private static final Logger LOG = LoggerFactory.getLogger(xxx.class);
LOG.error("error");
LOG.info("info");

logback的主要对象
1.LoggerContext    logback的核心对象,加载配置文件,存储loggerList……是log back的核心容器
2.Logger    这个Logger为logback的logger 它实现了slf4j的Logger对象,除了实现了所有的logger方法外,我们动态配置日志输出源,也需要里面的 addAppender(),detachAppender()方法
3.Appender    日志输出的最终实现, doAppend(E e)方法,最终实现了日志的输出,如果自定义appender 需要最终实现该接口。
注:以上对象并非logback全部核心对象,对于今天的日志动态输出,仅仅涉及到以上核心对象。

logback默认加载 classpath:logback.xml 配置,打印日志到文件中原理是通过 Appender 接口来实现不同的打印,logback原理请看:https://blog.csdn.net/qq_26462567/category_9307914.html

 打印日志到文件中使用的 RollingFileAppender ,LOG.info("info");跟踪源码可以发现最后会调用当前Logger引用的Appender.doAppend 方法:

AppenderAttachableImpl:

 所以我们重写doAppend

public class DynamicRollingFileAppender<E> extends RollingFileAppender<E>{

    /**
     * 日志打印会调用此方法,进行复写,判断租户,根据租户打印到不同日志文件
     * @param eventObject
     */
    public void doAppend(E eventObject) {
        String tenantType = RequestContext.getContext().getTenantType();
        if(StringUtils.isBlank(tenantType)){
            return;
            // throw new IllegalArgumentException("当前请求未找到租户类型");
        }
        //  this.getName() 是在logback.xml中配置的<appender name="appenderName" class="com.zh.log.core.logback.DynamicRollingFileAppender">
        // 只打印当前租户的Append,RollingFileAppender追加器以租户类型标识开头的执行追加
        if(this.getName().startsWith(tenantType)){
            super.doAppend(eventObject);
        }
    }
 
}

RequestContext 是当前线程文本类,存储当前线程中的一些变量以及租户信息,

TenantType是枚举类,定义租户信息,读者自行定义,这里不提供了。

public class RequestContext {

   private static final String TENANT_TYPE = "TENANT_TYPE";

   private Map<String, Object> values = new HashMap<String, Object>();
   
   private static final ThreadLocal<RequestContext> LOCAL = new ThreadLocal<RequestContext>() {
      @Override
      protected RequestContext initialValue() {
         return new RequestContext();
      }
   };

   public static RequestContext getContext() {
       return LOCAL.get();
   }
   
   public static void clearContext() {
       LOCAL.remove();
       LOCAL.set(new RequestContext());
   }
   
   public Object get(String key) {
        return values.get(key);
    }
   
   public void remove(String key) {
      values.remove(key);
   }
   
   public RequestContext set(String key, Object value) {
        if (value == null) {
            values.remove(key);
        } else {
            values.put(key, value);
        }
        return this;
    }

   /**
    * 设置数据源
    * @param value
    * @return
    */
   public RequestContext setDataSource(String value) {
      if (value == null) {
         values.remove(TENANT_TYPE);
      } else {
         values.put(TENANT_TYPE, value);
      }
      return this;
   }

   /**
    * 设置数据源
    * @param value
    * @return
    */
   public RequestContext setTenantType(String value) {
      if (value == null) {
         values.remove(TENANT_TYPE);
      } else {
         values.put(TENANT_TYPE, value);
      }
      return this;
   }

   /**
    * Get current DataSource
    *
    * @return data source name
    */
   public String getTenantType() {
      return (String) values.get(TENANT_TYPE);
   }

   /**
    * Get current DataSource
    *
    * @return data source name
    */
   public String getRedisPre() {
      if(StringUtils.isBlank(this.getTenantType())){
         throw new IllegalArgumentException("当前请求未找到租户类型");
      }
      return TenantType.getRedisPre(this.getTenantType());
   }

   /**
    * Get current DataSource
    *
    * @return data source name
    */
   public String getDataSource() {
      return (String) values.get(TENANT_TYPE);
   }

   /**
    * Clear current DataSource
    *
    * @return data source name
    */
   public void clearDataSource() {
      remove(TENANT_TYPE);
   }

}

 利用 Filter 过滤器在请求进入后,根据请求域名来判断租户(每个租户的域名请求是不同的),并且把租户保存到 RequestContext 中,供当前后续代码获取。

public class SwitchTenantFilter implements Filter {
   /** 日志 */
   private static Logger logger = LoggerFactory.getLogger(SwitchTenantFilter.class);

   @Override
   public void doFilter(ServletRequest req, ServletResponse resp,
         FilterChain filterChain) throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) resp;

      String host = request.getHeader("host");

      // 租户1访问
      if(host.contains("test1")){
         RequestContext.getContext().setDataSource("test1");
      }

      // 租户2访问
      if(host.contains("test2")){
         RequestContext.getContext().setDataSource("test2");
      }

      String dataSource = RequestContext.getContext().getDataSource();
      if(StringUtils.isBlank(dataSource)){
         logger.error("访问host:{},切换数据源失败!!!", host);
         filterChain.doFilter(request, response);
         return;
      }
      logger.info("访问host:{},切换数据源成功,数据源key:{}", host, RequestContext.getContext().getDataSource());
      filterChain.doFilter(request, response);
      // 清除 ThreadLocal 变量
   }
   
   @Override
   public void init(FilterConfig filterConfig) throws ServletException {
      // TODO Auto-generated method stub
      
   }

   @Override
   public void destroy() {
      // TODO Auto-generated method stub
      
   }

}

在Configuration类中实例化这个Filter  Bean,配置拦截

//  ===SwitchTenantFilter  切换租户,在WebAccessLogFilter之前加载===
    @Bean
    public FilterRegistrationBean switchDataSourceFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        SwitchTenantFilter switchTenantFilter = new SwitchTenantFilter();
        registration.setFilter(switchTenantFilter);

        registration.addUrlPatterns("/*");
        registration.setName("switchDataSourceFilter");
        registration.setOrder(12);
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        return registration;
    }
// ===SwitchTenantFilter===

最后配置logback.xml,将自定义的Appender配置进去,每个租户配置一个Appender,配置打印路径以及文件大小切换策略,把所有Appender在root中引用,打印会一层一层往上调用,最后会调用到root根logger,调用doAppend方法。

logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

    <property name="test1PATH" value="./logs/test1/" />

    <property name="test2PATH" value="./logs/test2/" />

    <property name="APPNAME" value="test" />

    <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t${APPNAME}\t%level\t%thread\t%logger{0}\t%logger{5}:%line\t%userName\t%clientIp:%clientPort\t%message%n</pattern>
        </encoder>
    </appender>

    <appender name="test1Appender"
              class="com.test1.test.core.logback.DynamicRollingFileAppender">
        <file>${test1PATH}test1.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${test1PATH}test1-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%level\t%thread\t%logger{0}\t%logger{5}:%line\t%userName\t%clientIp:%clientPort\t%message%n</pattern>
        </encoder>
    </appender>

    <appender name="test2Appender"
              class="com.test1.test.core.logback.DynamicRollingFileAppender">
        <file>${test2PATH}test2.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${test2PATH}test2-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%level\t%thread\t%logger{0}\t%logger{5}:%line\t%userName\t%clientIp:%clientPort\t%message%n</pattern>
        </encoder>
    </appender>

    <appender name="test1MysqlAppender"
              class="com.test1.test.core.logback.DynamicRollingFileAppender">
        <file>${test1PATH}test1-mysql.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${test1PATH}test1-mysql-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%thread\t%userName\t%clientIp\t%message%n</pattern>
        </encoder>
    </appender>

    <appender name="test2MysqlAppender"
              class="com.test1.test.core.logback.DynamicRollingFileAppender">
        <file>${test2PATH}test2-mysql.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${test2PATH}test2-mysql-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%thread\t%userName\t%clientIp\t%message%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="test1Appender" />
        <appender-ref ref="test2Appender" />
        <appender-ref ref="consoleAppender" />
    </root>
</configuration>

参考:

https://blog.csdn.net/qq_26462567/category_9307914.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值