当前位置: 首页 > 科技观察

日志配置热更新技术实践

时间:2023-03-16 17:46:37 科技观察

1、为什么需要服务日志热更新?对于后端老手,你一定遇到过这样的场景:为了排查线上突发问题,你很希望看到请求在服务链接上完整的日志输出;但是,在生产环境中,为了避免日志打印过多造成磁盘空间浪费,通常会将日志级别设置为INFO,关闭一般不用的日志输出;在重启服务的情况下,是否可以开启已经关闭的业务日志的输出?答案当然没有问题。2、需求分析熟悉logback的同学肯定想到过通过扫描监听logback.xml文件的配置变化来调整日志级别,如下:但通常情况下,你的业务服务是分布式部署的,有多个后端节点。如果你一个一个改,别说运维小哥未必会同意给你生产机文件的修改权限,就算有可能,也未免太“老实”了;有没有一种解决方案可以集中管理日志配置,修改文件然后逐个分发到各个节点?顺着这个思路,自然而然的就想到了配置中心,这里主要介绍一下携程开源的apollo。类似的配置中心产品还有百度Disconf、阿里ACM和SpringCloudConfig。有兴趣的可以自己研究。熟悉Apollo文件管理的同学都知道,Apollo通过推拉组合的方式将存储在服务器上的应用配置文件以属性的形式缓存到本地,如下图:demo+dev+logback.xml。propertiescontent=\n\n\t\n\n\t\n\t\t\n\t\t\t%d{yyyy-MM-ddHH\:mm\:ss.SSS}|%X{requestId}|[%t]%-5level%logger{50}%line-%m%n\n\t\t\n\t\n\n\t\n\t\tlogs/brm.log\n\t\t\n\t\t\t<模式>%d{yyyy-MM-ddHH\:mm\:ss.SSS}|%X{requestId}|%X{requestSeq}|[%t]%-5level%logger{50}%line-%m%n\n\t\t\n\t\t\n\t\t\tlogs/brm-%d{yyyy-MM-dd-HH}-%i.log>\n\t\t\t<\!--单个文件切割阈值,超过新日志文件的生成-->\n\t\t\t200MB\n\t\t\t<\!--最长保留天数-->\n\t\t\t336\n\t\t\n\t\n\n<\!--log4jdbc-->\n\n\n\n\n\n\n\n\t<rootlevel\="INFO">\n\t\t\n\t\t\n\t\nHH\:mm\:ss.SSS}|%X{requestId}|%X{requestSeq}|[%t]%-5level%logger{50}%line-%m%n\n\t\t\n\t\t\n\t\t\tlogs/brm-%d{yyyy-MM-dd-HH}-%i.log>\n\t\t\t<\!--单个文件切割阈值,超过新日志文件的生成-->\n\t\t\t200MB\n\t\t\t<\!--最大保留天数-->\n\t\t\t336\n\t\t\n\t\n\n<\!--log4jdbc-->\n\n\n\n\n\n\n\n\t\n\t\t\n\t\t\n\t\n而我们在配置logback的时候一般都会使用xml文件;所以,我们得想办法让logback加载context的内存值信息。JoranConfigurator读取logback数据后支持我们自定义Logback配置,springboot通过LoggingSystem加载管理日志系统;如果能在springboot启动的时候指定我自定义的日志加载类,问题就迎刃而解了。这里,我们在resources目录下新建一个META-INF文件夹,添加spring.factories,内容如下:org.springframework.context.ApplicationContextInitializer=com.zhoupu.zplog.refresher.LoggerRefresherorg.springframework.boot.env。EnvironmentPostProcessor=com.zhoupu.zplog.refresher.LoggerRefresher这里定义了一个LoggerRefresher,重写了loadDefaults和loadConfiguration方法,通过JoranConfigurator加载logback配置,并在configureByApollo中添加了apollo事件监听器。当发现logback.xml文件发生变化时,重新执行configureByApollo方法刷新日志配置。核心代码部分如下:packagecom.zhoupu.zplog.refresher;importch.qos.logback.classic.LoggerContext;importch.qos.logback.classic.joran.JoranConfigurator;importch.qos.logback.core.joran.spi.JoranException;importcom.ctrip.framework.apollo.Config;importcom.ctrip.framework.apollo.ConfigChangeListener;importcom.ctrip.framework.apollo.ConfigService;importcom.ctrip.framework.apollo.model.ConfigChangeEvent;importcom.ctrip.framework.apollo.spring.config.PropertySourcesConstants;importorg.slf4j.ILoggerFactory;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.env.EnvironmentPostProcessor;importorg.springframework.context.ApplicationContextInitializer;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.core.Ordered;importorg.springframework.core.env.ConfigurableEnvironment;importorg.springframework.util.StringUtils;importjavax.xml.parsers.DocumentBuilder;importjavax.xml.parsers.DocumentBuilderFactory;importjava.io.ByteArrayInputStream;importjava.io.UnsupportedEncodingException;/****@authorvigor*@date2019/6/14上午11:27*/publicclassLoggerRefresherimplementsApplicationContextInitializer,EnvironmentPostProcessor,Ordered{privatestaticfinalLoggerlog=LoggerFactory.getLogger(LoggerRefresher.class);privatebooleanloadFlag=false;@Overridepublicvoidinitialize(ConfigurableApplicationContextcontext){ConfigurableEnvironmentenvironment=context.getEnvironment();load(environment);}@OverridepublicvoidpostProcessEnvironment(ConfigurableEnvironmentenvironment,SpringApplicationapplication){load(environment);}@OverridepublicintgetOrder(){return1;}privatevoidload(ConfigurableEnvironmentenvironment){if(!loadFlag){environment.getPropertySources().forEach(ps->{if(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME.equals(ps.getName())){configureByApollo();loadFlag=true;}});}}privatevoidconfigureByApollo(){Configconfig=ConfigService.getConfig("logback.xml");Stringcontent=config.getProperty("content","");if(StringUtils.isEmpty(content)||!validateXML(content)){return;}config.addChangeListener(newConfigChangeListener(){@OverridepublicvoidonChange(ConfigChangeEventchangeEvent){configureByApollo();}@Overridepublicbooleanequals(Objectobj){if(this==obj){returntrue;}if(this.getClass().equals(obj.getClass())){returntrue;}returnfalse;}@OverridepublicinthashCode(){return1;}});ILoggerFactoryloggerFactory=LoggerFactory.getILoggerFactory();LoggerContextloggerContext=(LoggerContext)loggerFactory;loggerContext.reset();JoranConfiguratorconfigurator=newJoranConfigurator();configurator.setContext(loggerContext);try{configurator.doConfigure(newByteArrayInputStream(content.getBytes("utf-8")));log.warn("*****************************logbackconfigureByApollo成功!********************************");}catch(JoranExceptione){e.printStackTrace();}catch(UnsupportedEncodingExceptione){e.printStackTrace();}}privatebooleanvalidateXML(Stringxml){booleanisValidated=true;try{DocumentBuilderFactorydocumentBuilderFactory=DocumentBuilderFactory.newInstance();DocumentBuilderbuilder=documentBuilderFactory.newDocumentBuilder();builder.parse(newByteArrayInputStream(xml.getBytes("utf-8")));}catch(Exceptione){log.error("apollologbackconfigerror={}",e);isValidated=false;}returnisValidated;}}至此所有准备工作已经完成,运行demo程序,我的项目使用log4jdbc输出sql,这里我修改apollo配置管理后台jdbc日志配置,更改sqltiming级别信息:<loggername="jdbc.connection"level="OFF"/>发起后端请求,查看控制台日志输出,有!2019-11-0810:11:27.794|1fe97e7dcfeb4fc2810d8a7a706fad2a||-exec-3]INFOjdbc.sqltiming357-SELECTid,row_state,created_at,updated_at,created_by,updated_by,business_id,contact_name,role,mobile,contact_typeFROMt_business_contactWHERErow_state=0ANDbusiness_id=1000006surpriseisnotsurprise_,意思是不surprise!四总结一个简单的日志配置热更新尝试连接了logback的自定义配置加载原理,apollo的配置中心使用方法和事件监听机制,以及springboot日志管理和自动组装等知识点。希望大家能从中有所收获!【本文为专栏组织《周朴数据》原创文章微信公众号“周朴数据(id:zhoupudata)”】点此查看本作者更多好文