一、简介 分布式应用必须有一套日志采集功能,目的是将分布在各个服务器节点上的应用日志文件采集到统一的服务器上,方便日志的查看。springCloud本身提供了基于elk的日志采集,但是由于使用logstash,会加大运维成本。这里将使用轻量级的方案。二、思路 我们的目的是提供轻量级的日志采集来代替logstash,日志最终还是会存进Elasticsearch。为了能轻量级的实现日志采集,并且避免对代码的侵入,我们可以扩展Logback的appender,也可以扩展log4j的appender。这样我们使用slf4j来记录日志的时候,日志自动会保存到Elasticsearch中,并且不用修改任何业务代码。三、自定义Logbackappender 我们先来看一下Logback的appender的Uml图,我们可以发现两个对我们有借鉴意义的类 UnsynchronizedAppenderBase提供了异步的日志记录DBAppender基于数据库的日志记录 这两个类还是比较简单的,具体的代码我就不详细解说了,请自行查阅属性注入 基本实现逻辑从UnsynchronizedAppenderBase和DBAppender已经能够知道了,现在把我们需要的信息注入到Appender中,这里需要如下的知识Logback标签注入属性 我们可以直接在Xml中用标签配置属性,这些标签只要名称和appender中的成员变量名一致,则会自动把标签中的属性注入到成员变量中。 我们举一个例子: xml这样配置profiletestprofileesTypedemoesTypewithJansitruewithJansiencoderpattern{CONSOLELOGPATTERNIDE}patterncharsetutf8charsetencoderappender 其中ElasticsearchAppender是我们自己实现的Appender。这里有一个profile标签,我们需要ElasticsearchAppender中成员变量的名称和该标签名一致,这样就可以把test值注入到成员变量profile中。protectedStringprofile;运行环境Spring配置信息注入属性 有些信息可能已经在spring中做了配置,我们不想要重复的配置,这个时候我们可以用springProperty标签来进行设置。scope:作用范围name:名称source:spring配置defaultValue:默认值,必须要指定 然后在标签中用上面的name属性作为占位符,类中的成员变量名和标签名一致。 我们举一个例子: xml这样配置springPropertyscopecontextnameapplicationNamesourcespring。application。namedefaultValuespringPropertyscopecontextnameprofilesourcespring。profiles。activedefaultValuedefaultspringPropertyscopecontextnameesUserNamesourceluminary。elasticsearch。usernamedefaultValueelasticspringPropertyscopecontextnameesPasswordsourceluminary。elasticsearch。passworddefaultValue123456springPropertyscopecontextnameesServersourceluminary。elasticsearch。serverdefaultValue127。0。0。1:9200springPropertyscopecontextnameesMultiThreadedsourceluminary。elasticsearch。multiThreadeddefaultValuetruespringPropertyscopecontextnameesMaxTotalConnectionsourceluminary。elasticsearch。maxTotalConnectiondefaultValue20springPropertyscopecontextnameesMaxTotalConnectionPerRoutesourceluminary。elasticsearch。maxTotalConnectionPerRoutedefaultValue5springPropertyscopecontextnameesDiscoveryEnabledsourceluminary。elasticsearch。discoveryEnableddefaultValuetruespringPropertyscopecontextnameesDiscorveryFrequencysourceluminary。elasticsearch。discorveryFrequencydefaultValue60{applicationName}applicationNameprofile{profile}profileesTypedemoesTypeusername{esUserName}usernamepassword{esPassword}passwordserver{esServer}servermultiThreaded{esMultiThreaded}multiThreadedmaxTotalConnection{esMaxTotalConnection}maxTotalConnectionmaxTotalConnectionPerRoute{esMaxTotalConnectionPerRoute}maxTotalConnectionPerRoutediscoveryEnabled{esDiscoveryEnabled}discoveryEnableddiscorveryFrequency{esDiscorveryFrequency}discorveryFrequencyappender yml这样配置spring:application:name:loggerdemoserverluminary:elasticsearch:username:elasticpassword:123456server:127。0。0。1:9200multiThreaded:truemaxTotalConnection:20maxTotalConnectionPerRoute:5discoveryEnabled:truediscorveryFrequency:60 成员变量SetterprotectedStringesIndexjavalogdate;索引SetterprotectedStringesTypejavalog;类型SetterprotectedbooleanisLocationInfotrue;是否打印行号SetterprotectedStringapplicationName;SetterprotectedStringprofile;运行环境SetterprotectedStringesAddress;地址Logback代码注入属性 这里还有一种情况,有些属性需要在运行时才知道,或者运行时会改变。这就需要能动态注入属性。我们可以使用log4j的MDC类来解决。 我们可以通过相应的put,remove方法来动态设置属性。 比如:MDC。put(TraceInfo。TRACEIDKEY,traceInfo。getTraceId());MDC。put(TraceInfo。RPCIDKEY,traceInfo。getRpcId());MDC。remove(TraceInfo。TRACEIDKEY);MDC。remove(TraceInfo。RPCIDKEY); 获取属性值可以通过LoggingEvent的getMDCPropertyMap方法先获取属性的map,再根据键名从map中取出来。 比如:privateStringgetRpcId(LoggingEventevent){MapString,StringmdcPropertyMapevent。getMDCPropertyMap();returnmdcPropertyMap。get(rpcId);}privateStringgetTraceId(LoggingEventevent){MapString,StringmdcPropertyMapevent。getMDCPropertyMap();returnmdcPropertyMap。get(traceId);} 值得说明的是,mdcAdapter是一个静态的成员变量,但是它自身是线程安全的,我们可以看一下logback的实现privateMapString,StringduplicateAndInsertNewMap(MapString,StringoldMap){MapString,StringnewMapCollections。synchronizedMap(newHashMapString,String());if(oldMap!null){wedontwanttheparentthreadmodifyingoldMapwhileweareiteratingoveritsynchronized(oldMap){newMap。putAll(oldMap);}}copyOnThreadLocal。set(newMap);returnnewMap;}Elasticsearch模板设计 最后日志保存在Elasticsearch中,我们希望索引名为javalog{date}的形式,type名为实际的微服务名 最后我们对日志索引设置一个模板 举一个例子:PUTtemplatejavalog{template:javalog,order:0,setting:{index:{refreshinterval:5s}},mappings:{default:{dynamictemplates:〔{messagefield:{matchmappingtype:string,pathmatch:message,mapping:{norms:false,type:text,analyzer:ikmaxword,searchanalyzer:ikmaxword}}},{throwablefield:{matchmappingtype:string,pathmatch:throwable,mapping:{norms:false,type:text,analyzer:ikmaxword,searchanalyzer:ikmaxword}}},{stringfield:{matchmappingtype:string,match:,mapping:{norms:false,type:text,analyzer:ikmaxword,searchanalyzer:ikmaxword,fields:{keyword:{type:keyword}}}}}〕,all:{enabled:false},properties:{applicationName:{norms:false,type:text,analyzer:ikmaxword,searchanalyzer:ikmaxword,fields:{keyword:{type:keyword,ignoreabove:256}}},profile:{type:keyword},host:{type:keyword},ip:{type:ip},level:{type:keyword},location:{properties:{line:{type:integer}}},dateTime:{type:date},traceId:{type:keyword},rpcId:{type:keyword}}}}} 示例代码Slf4jpublicclassElasticsearchAppenderEextendsUnsynchronizedAppenderBaseEimplementsLuminaryLoggerAppenderE{privatestaticfinalFastDateFormatSIMPLEFORMATFastDateFormat。getInstance(yyyyMMdd);privatestaticfinalFastDateFormatISODATETIMETIMEZONEFORMATWITHMILLISFastDateFormat。getInstance(yyyyMMddTHH:mm:ss。SSSZZ);protectedJestClientjestClient;privatestaticfinalStringCONFIGPROPERTIESNAMEes。properties;可在xml中配置的属性SetterprotectedStringesIndexjavalogdate;索引SetterprotectedStringesTypejavalog;类型SetterprotectedbooleanisLocationInfotrue;是否打印行号SetterprotectedStringapplicationName;SetterprotectedStringprofile;运行环境SetterprotectedStringesAddress;地址Overridepublicvoidstart(){super。start();init();}Overridepublicvoidstop(){super。stop();关闭es客户端try{jestClient。close();}catch(IOExceptione){addStatus(newErrorStatus(closejestClientfail,this,e));}}Overrideprotectedvoidappend(Eevent){if(!isStarted()){return;}subAppend(event);}privatevoidsubAppend(Eevent){if(!isStarted()){return;}try{thisstepavoidsLBCLASSIC139if(eventinstanceofDeferredProcessingAware){((DeferredProcessingAware)event)。prepareForDeferredProcessing();}thesynchronizationpreventstheOutputStreamfrombeingclosedwhilewearewriting。Italsopreventsmultiplethreadsfromenteringthesameconverter。Convertersassumethattheyareinasynchronizedblock。save(event);}catch(Exceptionioe){assoonasanexceptionoccurs,movetononstartedstateandaddasingleErrorStatustotheSM。this。startedfalse;addStatus(newErrorStatus(IOfailureinappender,this,ioe));}}privatevoidsave(Eevent){if(eventinstanceofLoggingEvent){获得ahrefhttps:www。bs178。comrizhitargetblankclassinfotextkey日志a数据EsLogVOesLogVOcreateData((LoggingEvent)event);保存到es中save(esLogVO);}else{addWarn(theerrortypeofevent!);}}privatevoidsave(EsLogVOesLogVO){GsongsonnewGson();StringjsonStringgson。toString();StringesIndexFormatesIndex。replace(date,SIMPLEFORMAT。format(Calendar。getInstance()。getTime()));IndexindexnewIndex。Builder(esLogVO)。index(esIndexFormat)。type(esType)。build();try{DocumentResultresultjestClient。execute(index);addStatus(newInfoStatus(esloggerresult:result。getJsonString(),this));}catch(Exceptione){addStatus(newErrorStatus(jestClientexecfail,this,e));}}privateEsLogVOcreateData(LoggingEventevent){EsLogVOesLogVOnewEsLogVO();获得applicationNameesLogVO。setApplicationName(applicationName);获得profileesLogVO。setProfile(profile);获得ipesLogVO。setIp(HostUtil。getIP());获得hostNameesLogVO。setHost(HostUtil。getHostName());获得时间longdateTimegetDateTime(event);esLogVO。setDateTime(ISODATETIMETIMEZONEFORMATWITHMILLIS。format(Calendar。getInstance()。getTime()));获得线程StringthreadNamegetThead(event);esLogVO。setThread(threadName);获得ahrefhttps:www。bs178。comrizhitargetblankclassinfotextkey日志a等级StringlevelgetLevel(event);esLogVO。setLevel(level);获得调用信息EsLogVO。LocationlocationgetLocation(event);esLogVO。setLocation(location);获得ahrefhttps:www。bs178。comrizhitargetblankclassinfotextkey日志a信息StringmessagegetMessage(event);esLogVO。setMessage(message);获得异常信息StringthrowablegetThrowable(event);esLogVO。setThrowable(throwable);获得traceIdStringtraceIdgetTraceId(event);esLogVO。setTraceId(traceId);获得rpcIdStringrpcIdgetRpcId(event);esLogVO。setRpcId(rpcId);returnesLogVO;}privateStringgetRpcId(LoggingEventevent){MapString,StringmdcPropertyMapevent。getMDCPropertyMap();returnmdcPropertyMap。get(rpcId);}privateStringgetTraceId(LoggingEventevent){MapString,StringmdcPropertyMapevent。getMDCPropertyMap();returnmdcPropertyMap。get(traceId);}privateStringgetThrowable(LoggingEventevent){StringexceptionStack;IThrowableProxytpevent。getThrowableProxy();if(tpnull)return;StringBuildersbnewStringBuilder(2048);while(tp!null){StackTraceElementProxy〔〕stackArraytp。getStackTraceElementProxyArray();ThrowableProxyUtil。subjoinFirstLine(sb,tp);intcommonFramestp。getCommonFrames();StackTraceElementProxy〔〕stepArraytp。getStackTraceElementProxyArray();for(inti0;istepArray。lengthcommonFrames;i){sb。append();sb。append(CoreConstants。TAB);ThrowableProxyUtil。subjoinSTEP(sb,stepArray〔i〕);}if(commonFrames0){sb。append();sb。append(CoreConstants。TAB)。append(。。。)。append(commonFrames)。append(commonframesomitted);}sb。append();tptp。getCause();}returnsb。toString();}privateStringgetMessage(LoggingEventevent){returnevent。getFormattedMessage();}privateEsLogVO。LocationgetLocation(LoggingEventevent){EsLogVO。LocationlocationnewEsLogVO。Location();if(isLocationInfo){StackTraceElement〔〕cdaevent。getCallerData();if(cda!nullcda。length0){StackTraceElementimmediateCallerDatacda〔0〕;location。setClassName(immediateCallerData。getClassName());location。setMethod(immediateCallerData。getMethodName());location。setFile(immediateCallerData。getFileName());location。setLine(String。valueOf(immediateCallerData。getLineNumber()));}}returnlocation;}privateStringgetLevel(LoggingEventevent){returnevent。getLevel()。toString();}privateStringgetThead(LoggingEventevent){returnevent。getThreadName();}privatelonggetDateTime(LoggingEventevent){return((LoggingEvent)event)。getTimeStamp();}privatevoidinit(){try{ClassLoaderesClassLoaderElasticsearchAppender。class。getClassLoader();SetURLesConfigPathSetnewLinkedHashSetURL();EnumerationURLpaths;if(esClassLoadernull){pathsClassLoader。getSystemResources(CONFIGPROPERTIESNAME);}else{pathsesClassLoader。getResources(CONFIGPROPERTIESNAME);}while(paths。hasMoreElements()){URLpathpaths。nextElement();esConfigPathSet。add(path);}if(esConfigPathSet。size()0){subInit();if(jestClientnull){addWarn(没有获取到配置信息!);用默认信息初始化es客户端jestClientnewJestClientMgr()。getJestClient();}}else{if(esConfigPathSet。size()1){addWarn(获取到多个配置信息,将以第一个为准!);}URLpathesConfigPathSet。iterator()。next();try{PropertiesconfignewProperties();CleanupInputStreaminputnewFileInputStream(path。getPath());config。load(input);通过properties初始化es客户端jestClientnewJestClientMgr(config)。getJestClient();}catch(Exceptione){addStatus(newErrorStatus(configfail,this,e));}}}catch(Exceptione){addStatus(newErrorStatus(configfail,this,e));}}OverridepublicvoidsubInit(){templatemethod}}代码地址: https:github。comwulinfeng2luminarycomponent 原文链接:https:mp。weixin。qq。comsDxkoOTBrFUAgnKGj7Y6Cg