一、简介
分布式应用必须有一套日志采集功能,目的是将分布在各个服务器节点上的应用日志文件采集到统一的服务器上,方便日志的查看。springCloud本身提供了基于elk的日志采集,但是由于使用logstash,会加大运维成本。这里将使用轻量级的方案。
二、思路
我们的目的是提供轻量级的日志采集来代替logstash,日志最终还是会存进Elasticsearch。为了能轻量级的实现日志采集,并且避免对代码的侵入,我们可以扩展Logback的appender,也可以扩展log4j的appender。这样我们使用slf4j来记录日志的时候,日志自动会保存到Elasticsearch中,并且不用修改任何业务代码。
三、自定义Logback appender
我们先来看一下Logback的appender的Uml图,我们可以发现两个对我们有借鉴意义的类
这两个类还是比较简单的,具体的代码我就不详细解说了,请自行查阅
属性注入
基本实现逻辑从UnsynchronizedAppenderBase
和DBAppender
已经能够知道了,现在把我们需要的信息注入到Appender
中,这里需要如下的知识
Logback标签注入属性
我们可以直接在Xml中用标签配置属性,这些标签只要名称和appender
中的成员变量名一致,则会自动把标签中的属性注入到成员变量中。
我们举一个例子:
xml这样配置
testdemotrue${CONSOLE_LOG_PATTERN_IDE}utf8
其中ElasticsearchAppender
是我们自己实现的Appender。这里有一个profile标签,我们需要ElasticsearchAppender
中成员变量的名称和该标签名一致,这样就可以把test值注入到成员变量profile中。
protected String profile = ""; // 运行环境
Spring配置信息注入属性
有些信息可能已经在spring中做了配置,我们不想要重复的配置,这个时候我们可以用springProperty
标签来进行设置。
然后在标签中用上面的name属性作为占位符,类中的成员变量名和标签名一致。
我们举一个例子:
xml这样配置
${applicationName}${profile}demo${esUserName}${esPassword}${esServer}${esMultiThreaded}${esMaxTotalConnection}${esMaxTotalConnectionPerRoute}${esDiscoveryEnabled}${esDiscorveryFrequency}
yml这样配置
spring:application:name: logger-demo-serverluminary: elasticsearch:username: elasticpassword: 123456server: - 127.0.0.1:9200multiThreaded: truemaxTotalConnection: 20maxTotalConnectionPerRoute: 5discoveryEnabled: truediscorveryFrequency: 60
成员变量
@Setter
protected String esIndex = "java-log-#date#"; // 索引
@Setter
protected String esType = "java-log"; // 类型
@Setter
protected boolean isLocationInfo = true; // 是否打印行号
@Setter
protected String applicationName = "";
@Setter
protected String profile = ""; // 运行环境
@Setter
protected String esAddress = ""; // 地址
Logback代码注入属性
这里还有一种情况,有些属性需要在运行时才知道,或者运行时会改变。这就需要能动态注入属性。我们可以使用log4j的MDC类来解决。
我们可以通过相应的put,remove方法来动态设置属性。
比如:
MDC.put(TraceInfo.TRACE_ID_KEY, traceInfo.getTraceId());
MDC.put(TraceInfo.RPC_ID_KEY, traceInfo.getRpcId());
MDC.remove(TraceInfo.TRACE_ID_KEY);
MDC.remove(TraceInfo.RPC_ID_KEY);
获取属性值可以通过LoggingEvent
的getMDCPropertyMap
方法先获取属性的map,再根据键名从map中取出来。
比如:
private String getRpcId(LoggingEvent event) {Map mdcPropertyMap = event.getMDCPropertyMap();return mdcPropertyMap.get("rpcId");
}private String getTraceId(LoggingEvent event) {Map mdcPropertyMap = event.getMDCPropertyMap();return mdcPropertyMap.get("traceId");
}
值得说明的是,mdcAdapter是一个静态的成员变量,但是它自身是线程安全的,我们可以看一下logback的实现
private Map duplicateAndInsertNewMap(Map oldMap) {Map newMap = Collections.synchronizedMap(new HashMap());if (oldMap != null) {// we don't want the parent thread modifying oldMap while we are// iterating over itsynchronized (oldMap) {newMap.putAll(oldMap);}}copyOnThreadLocal.set(newMap);return newMap;}
Elasticsearch模板设计
最后日志保存在Elasticsearch中,我们希望索引名为java-log-${date}
的形式,type名为实际的微服务名
最后我们对日志索引设置一个模板
举一个例子:
PUT _template/java-log
{"template": "java-log-*","order": 0,"setting": {"index": {"refresh_interval": "5s"}},"mappings": {"_default_": {"dynamic_templates": [{"message_field": {"match_mapping_type": "string","path_match": "message","mapping": {"norms": false,"type": "text","analyzer": "ik_max_word","search_analyzer": "ik_max_word"}}},{"throwable_field": {"match_mapping_type": "string","path_match": "throwable","mapping": {"norms": false,"type": "text","analyzer": "ik_max_word","search_analyzer": "ik_max_word"}}},{"string_field": {"match_mapping_type": "string","match": "*","mapping": {"norms": false,"type": "text","analyzer": "ik_max_word","search_analyzer": "ik_max_word","fields": {"keyword": {"type": "keyword"}}}}}],"_all": {"enabled": false},"properties": {"applicationName": {"norms": false,"type": "text","analyzer": "ik_max_word","search_analyzer": "ik_max_word","fields": {"keyword": {"type": "keyword","ignore_above": 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"}}}}
}
示例代码
@Slf4j
public class ElasticsearchAppender extends UnsynchronizedAppenderBase implements LuminaryLoggerAppender {private static final FastDateFormat SIMPLE_FORMAT &#61; FastDateFormat.getInstance("yyyy-MM-dd");private static final FastDateFormat ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS &#61; FastDateFormat.getInstance("yyyy-MM-dd&#39;T&#39;HH:mm:ss.SSSZZ");protected JestClient jestClient;private static final String CONFIG_PROPERTIES_NAME &#61; "es.properties";// 可在xml中配置的属性&#64;Setterprotected String esIndex &#61; "java-log-#date#"; // 索引&#64;Setterprotected String esType &#61; "java-log"; // 类型&#64;Setterprotected boolean isLocationInfo &#61; true; // 是否打印行号&#64;Setterprotected String applicationName &#61; "";&#64;Setterprotected String profile &#61; ""; // 运行环境&#64;Setterprotected String esAddress &#61; ""; // 地址&#64;Overridepublic void start() {super.start();init();}&#64;Overridepublic void stop() {super.stop();// 关闭es客户端try {jestClient.close();} catch (IOException e) {addStatus(new ErrorStatus("close jestClient fail", this, e));}}&#64;Overrideprotected void append(E event) {if (!isStarted()) {return;}subAppend(event);}private void subAppend(E event) {if (!isStarted()) {return;}try {// this step avoids LBCLASSIC-139if (event instanceof DeferredProcessingAware) {((DeferredProcessingAware) event).prepareForDeferredProcessing();}// the synchronization prevents the OutputStream from being closed while we// are writing. It also prevents multiple threads from entering the same// converter. Converters assume that they are in a synchronized block.save(event);} catch (Exception ioe) {// as soon as an exception occurs, move to non-started state// and add a single ErrorStatus to the SM.this.started &#61; false;addStatus(new ErrorStatus("IO failure in appender", this, ioe));}}private void save(E event) {if(event instanceof LoggingEvent) {// 获得日志数据EsLogVO esLogVO &#61; createData((LoggingEvent) event);// 保存到es中save(esLogVO);} else {addWarn("the error type of event!");}}private void save(EsLogVO esLogVO) {Gson gson &#61; new Gson();String jsonString &#61; gson.toString();String esIndexFormat &#61; esIndex.replace("#date#", SIMPLE_FORMAT.format(Calendar.getInstance().getTime()));Index index &#61; new Index.Builder(esLogVO).index(esIndexFormat).type(esType).build();try {DocumentResult result &#61; jestClient.execute(index);addStatus(new InfoStatus("es logger result:"&#43;result.getJsonString(), this));} catch (Exception e) {addStatus(new ErrorStatus("jestClient exec fail", this, e));}}private EsLogVO createData(LoggingEvent event) {EsLogVO esLogVO &#61; new EsLogVO();// 获得applicationNameesLogVO.setApplicationName(applicationName);// 获得profileesLogVO.setProfile(profile);// 获得ipesLogVO.setIp(HostUtil.getIP());// 获得hostNameesLogVO.setHost(HostUtil.getHostName());// 获得时间long dateTime &#61; getDateTime(event);esLogVO.setDateTime(ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS.format(Calendar.getInstance().getTime()));// 获得线程String threadName &#61; getThead(event);esLogVO.setThread(threadName);// 获得日志等级String level &#61; getLevel(event);esLogVO.setLevel(level);// 获得调用信息EsLogVO.Location location &#61; getLocation(event);esLogVO.setLocation(location);// 获得日志信息String message &#61; getMessage(event);esLogVO.setMessage(message);// 获得异常信息String throwable &#61; getThrowable(event);esLogVO.setThrowable(throwable);// 获得traceIdString traceId &#61; getTraceId(event);esLogVO.setTraceId(traceId);// 获得rpcIdString rpcId &#61; getRpcId(event);esLogVO.setRpcId(rpcId);return esLogVO;}private String getRpcId(LoggingEvent event) {Map mdcPropertyMap &#61; event.getMDCPropertyMap();return mdcPropertyMap.get("rpcId");}private String getTraceId(LoggingEvent event) {Map mdcPropertyMap &#61; event.getMDCPropertyMap();return mdcPropertyMap.get("traceId");}private String getThrowable(LoggingEvent event) {String exceptionStack &#61; "";IThrowableProxy tp &#61; event.getThrowableProxy();if (tp &#61;&#61; null)return "";StringBuilder sb &#61; new StringBuilder(2048);while (tp !&#61; null) {StackTraceElementProxy[] stackArray &#61; tp.getStackTraceElementProxyArray();ThrowableProxyUtil.subjoinFirstLine(sb, tp);int commonFrames &#61; tp.getCommonFrames();StackTraceElementProxy[] stepArray &#61; tp.getStackTraceElementProxyArray();for (int i &#61; 0; i < stepArray.length - commonFrames; i&#43;&#43;) {sb.append("\n");sb.append(CoreConstants.TAB);ThrowableProxyUtil.subjoinSTEP(sb, stepArray[i]);}if (commonFrames > 0) {sb.append("\n");sb.append(CoreConstants.TAB).append("... ").append(commonFrames).append(" common frames omitted");}sb.append("\n");tp &#61; tp.getCause();}return sb.toString();}private String getMessage(LoggingEvent event) {return event.getFormattedMessage();}private EsLogVO.Location getLocation(LoggingEvent event) {EsLogVO.Location location &#61; new EsLogVO.Location();if(isLocationInfo) {StackTraceElement[] cda &#61; event.getCallerData();if (cda !&#61; null && cda.length > 0) {StackTraceElement immediateCallerData &#61; cda[0];location.setClassName(immediateCallerData.getClassName());location.setMethod(immediateCallerData.getMethodName());location.setFile(immediateCallerData.getFileName());location.setLine(String.valueOf(immediateCallerData.getLineNumber()));}}return location;}private String getLevel(LoggingEvent event) {return event.getLevel().toString();}private String getThead(LoggingEvent event) {return event.getThreadName();}private long getDateTime(LoggingEvent event) {return ((LoggingEvent) event).getTimeStamp();}private void init() {try {ClassLoader esClassLoader &#61; ElasticsearchAppender.class.getClassLoader();Set esConfigPathSet &#61; new LinkedHashSet();Enumeration paths;if (esClassLoader &#61;&#61; null) {paths &#61; ClassLoader.getSystemResources(CONFIG_PROPERTIES_NAME);} else {paths &#61; esClassLoader.getResources(CONFIG_PROPERTIES_NAME);}while (paths.hasMoreElements()) {URL path &#61; paths.nextElement();esConfigPathSet.add(path);}if(esConfigPathSet.size() &#61;&#61; 0) {subInit();if(jestClient &#61;&#61; null) {addWarn("没有获取到配置信息&#xff01;");// 用默认信息初始化es客户端jestClient &#61; new JestClientMgr().getJestClient();}} else {if (esConfigPathSet.size() > 1) {addWarn("获取到多个配置信息,将以第一个为准&#xff01;");}URL path &#61; esConfigPathSet.iterator().next();try {Properties config &#61; new Properties();&#64;Cleanup InputStream input &#61; new FileInputStream(path.getPath());config.load(input);// 通过properties初始化es客户端jestClient &#61; new JestClientMgr(config).getJestClient();} catch (Exception e) {addStatus(new ErrorStatus("config fail", this, e));}}} catch (Exception e) {addStatus(new ErrorStatus("config fail", this, e));}}&#64;Overridepublic void subInit() {// template method}}
代码地址&#xff1a;
https://github.com/wulinfeng2/luminary-component