日志配置熱更新技術(shù)實(shí)踐
一 為什么需要服務(wù)日志熱更新?
對(duì)于后端老鳥來說,一定遇到過這樣的場(chǎng)景:
為了排查線上突發(fā)的問題,非常希望能夠全面的看到請(qǐng)求在服務(wù)鏈路上的完整日志輸出;
But,在生產(chǎn)環(huán)境中,為了避免日志打印過量造成磁盤空間浪費(fèi),通常會(huì)將日志級(jí)別設(shè)定在INFO,并關(guān)閉一般情況用不到的日志輸出;
在不重啟服務(wù)的情況下,開啟本已經(jīng)關(guān)閉的業(yè)務(wù)日志輸出,能不能搞的定呢?答案是當(dāng)然沒問題。
二 需求分析
熟悉logback的同學(xué)此時(shí)肯定已經(jīng)想到通過掃描監(jiān)聽logback.xml文件配置變化來實(shí)現(xiàn)日志級(jí)別的調(diào)整,像如下這種方式:
- <configuration debug="true" scan="true" scanPeriod="1 seconds">
但通常情況下,你的業(yè)務(wù)服務(wù)是分布式部署的,后端節(jié)點(diǎn)有多臺(tái),如果一臺(tái)臺(tái)的去改,且不說運(yùn)維大哥未必就會(huì)同意給你生產(chǎn)機(jī)器文件的修改權(quán)限,即使可以,這么做未免有些過于“老實(shí)”了;有沒有一種可以集中管理日志配置,修改文件后再逐個(gè)分發(fā)給各節(jié)點(diǎn)的解決方案呢?沿著這個(gè)思路,自然而然就會(huì)聯(lián)想到配置中心,這里,我主要介紹攜程開源的apollo,同類的配置中心產(chǎn)品還有百度Disconf、阿里ACM和Spring Cloud Config,感興趣的自行研究。
三 做實(shí)驗(yàn)
熟悉apollo文件管理的同學(xué)都知道,apollo通過推拉結(jié)合的方式將服務(wù)端存儲(chǔ)的應(yīng)用配置文件緩存到本地是以properties的格式存儲(chǔ)的,如下面所示:
demo+dev+logback.xml.properties
- content=<?xml version\="1.0" encoding\="UTF-8"?>\n<configuration debug\="true">\n\t<property name\="encoding" value\="UTF-8"/>\n\n\t<appender name\="STDOUT" class\="ch.qos.logback.core.ConsoleAppender">\n\t\t<encoder class\="ch.qos.logback.classic.encoder.PatternLayoutEncoder">\n\t\t\t<pattern>%d{yyyy-MM-dd HH\:mm\:ss.SSS}|%X{requestId}|[%t] %-5level %logger{50} %line - %m%n</pattern>\n\t\t</encoder>\n\t</appender>\n\n\t<appender name\="FILE" class\="ch.qos.logback.core.rolling.RollingFileAppender">\n\t\t<file>logs/brm.log</file>\n\t\t<encoder class\="ch.qos.logback.classic.encoder.PatternLayoutEncoder">\n\t\t\t<pattern>%d{yyyy-MM-dd HH\:mm\:ss.SSS}|%X{requestId}|%X{requestSeq}|[%t] %-5level %logger{50} %line - %m%n</pattern>\n\t\t</encoder>\n\t\t<rollingPolicy class\="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">\n\t\t\t<fileNamePattern>logs/brm-%d{yyyy-MM-dd-HH}-%i.log</fileNamePattern>>\n\t\t\t<\!--單個(gè)文件切割閾值,超過生成新log文件-->\n\t\t\t<maxFileSize>200MB</maxFileSize>\n\t\t\t<\!--最大保留天數(shù)-->\n\t\t\t<maxHistory>336</maxHistory>\n\t\t</rollingPolicy>\n\t</appender>\n\n <\!--log4jdbc -->\n <logger name\="jdbc.sqltiming" level\="INFO"/>\n <logger name\="jdbc.sqlonly" level\="OFF"/>\n <logger name\="jdbc.audit" level\="OFF"/>\n <logger name\="jdbc.resultset" level\="OFF"/>\n <logger name\="jdbc.resultsettable" level\="OFF"/>\n <logger name\="jdbc.connection" level\="OFF"/>\n \n\t<root level\="INFO">\n\t\t<appender-ref ref\="STDOUT"/>\n\t\t<appender-ref ref\="FILE"/>\n\t</root>\n</configuration>
而我們通常在配置logback的時(shí)候使用的是xml文件;
因此,我們要想辦法讓logback能夠加載context的內(nèi)存值信息。
閱讀logback資料發(fā)現(xiàn),JoranConfigurator支持我們以自定義的方式配置logback,
而springboot是通過LoggingSystem來加載管理日志系統(tǒng)的;如果我能在springboot啟動(dòng)的時(shí)候指定我自定義的日志加載類,問題便迎刃而解。
這里,我們?cè)趓esources目錄下新建META-INF文件夾,添加spring.factories,內(nèi)容如下:
- org.springframework.context.ApplicationContextInitializer = com.zhoupu.zplog.refresher.LoggerRefresher
- org.springframework.boot.env.EnvironmentPostProcessor = com.zhoupu.zplog.refresher.LoggerRefresher
這里我們定義一個(gè)LoggerRefresher,該類重寫loadDefaults和loadConfiguration方法,通過JoranConfigurator加載logback配置,并在configureByApollo中添加一個(gè)apollo事件監(jiān)聽器,當(dāng)發(fā)現(xiàn)logback.xml文件有變化時(shí),重新執(zhí)行configureByApollo方法刷新日志配置。
核心代碼部分如下:
- package com.zhoupu.zplog.refresher;
- import ch.qos.logback.classic.LoggerContext;
- import ch.qos.logback.classic.joran.JoranConfigurator;
- import ch.qos.logback.core.joran.spi.JoranException;
- import com.ctrip.framework.apollo.Config;
- import com.ctrip.framework.apollo.ConfigChangeListener;
- import com.ctrip.framework.apollo.ConfigService;
- import com.ctrip.framework.apollo.model.ConfigChangeEvent;
- import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
- import org.slf4j.ILoggerFactory;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.env.EnvironmentPostProcessor;
- import org.springframework.context.ApplicationContextInitializer;
- import org.springframework.context.ConfigurableApplicationContext;
- import org.springframework.core.Ordered;
- import org.springframework.core.env.ConfigurableEnvironment;
- import org.springframework.util.StringUtils;
- import javax.xml.parsers.DocumentBuilder;
- import javax.xml.parsers.DocumentBuilderFactory;
- import java.io.ByteArrayInputStream;
- import java.io.UnsupportedEncodingException;
- /**
- *
- * @author vigor
- * @date 2019/6/14 上午11:27
- */
- public class LoggerRefresher implements ApplicationContextInitializer<ConfigurableApplicationContext>, EnvironmentPostProcessor, Ordered {
- private static final Logger log = LoggerFactory.getLogger(LoggerRefresher.class);
- private boolean loadFlag = false;
- @Override
- public void initialize(ConfigurableApplicationContext context) {
- ConfigurableEnvironment environment = context.getEnvironment();
- load(environment);
- }
- @Override
- public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
- load(environment);
- }
- @Override
- public int getOrder() {
- return 1;
- }
- private void load(ConfigurableEnvironment environment) {
- if (!loadFlag) {
- environment.getPropertySources().forEach(ps -> {
- if (PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME.equals(ps.getName())) {
- configureByApollo();
- loadFlag = true;
- }
- });
- }
- }
- private void configureByApollo() {
- Config config = ConfigService.getConfig("logback.xml");
- String content = config.getProperty("content", "");
- if (StringUtils.isEmpty(content) || !validateXML(content)) {
- return;
- }
- config.addChangeListener(new ConfigChangeListener() {
- @Override
- public void onChange(ConfigChangeEvent changeEvent) {
- configureByApollo();
- }
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (this.getClass().equals(obj.getClass())) {
- return true;
- }
- return false;
- }
- @Override
- public int hashCode() {
- return 1;
- }
- });
- ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
- LoggerContext loggerContext = (LoggerContext) loggerFactory;
- loggerContext.reset();
- JoranConfigurator configurator = new JoranConfigurator();
- configurator.setContext(loggerContext);
- try {
- configurator.doConfigure(new ByteArrayInputStream(content.getBytes("utf-8")));
- log.warn("*****************************logback configureByApollo success!********************************");
- } catch (JoranException e) {
- e.printStackTrace();
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- }
- private boolean validateXML(String xml){
- boolean isValidated = true;
- try {
- DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
- DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
- builder.parse(new ByteArrayInputStream(xml.getBytes("utf-8")));
- } catch (Exception e) {
- log.error("apollo logback config error = {}", e);
- isValidated = false;
- }
- return isValidated;
- }
- }
至此已完成所有準(zhǔn)備工作,運(yùn)行demo程序,我的項(xiàng)目使用log4jdbc輸出sql,這里我通過修改apollo配置管理后臺(tái)jdbc日志配置,將sqltiming級(jí)別改為INFO:
- <!--log4jdbc -->
- <logger name="jdbc.sqltiming" level="INFO"/>
- <logger name="jdbc.sqlonly" level="OFF"/>
- <logger name="jdbc.audit" level="OFF"/>
- <logger name="jdbc.resultset" level="OFF"/>
- <logger name="jdbc.resultsettable" level="OFF"/>
- <logger name="jdbc.connection" level="OFF"/>
發(fā)起一個(gè)后端請(qǐng)求,查看控制臺(tái)日志輸出,有了!
- 2019-11-08 10:11:27.794|1fe97e7dcfeb4fc2810d8a7a706fad2a||[http-nio-8062-exec-3] INFO jdbc.sqltiming 357 - SELECT id, row_state, created_at, updated_at, created_by, updated_by, business_id, contact_name,
- role, mobile, contact_type FROM t_business_contact WHERE row_state = 0 AND business_id = 1000006
驚不驚喜_,意不意外!
四 總結(jié)
一個(gè)簡(jiǎn)單的日志配置熱更新嘗試,串聯(lián)起了logback的自定義配置加載原理,apollo的配置中心使用方法和事件監(jiān)聽機(jī)制,以及springboot日志管理和自動(dòng)裝配等知識(shí)點(diǎn),希望大家能從中有所收獲!
【本文是51CTO專欄機(jī)構(gòu)“舟譜數(shù)據(jù)”的原創(chuàng)文章,微信公眾號(hào)“舟譜數(shù)據(jù)( id: zhoupudata)”】