热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java微服务之从0开始搭建SpringCloud项目分布式日志架构技术栈

一、简介分布式应用必须有一套日志采集功能,目的是将分布在各个服务器节点上的应用日志文件采集到统一的服务器上,方便日志的查看。springCloud本身

 

一、简介

分布式应用必须有一套日志采集功能,目的是将分布在各个服务器节点上的应用日志文件采集到统一的服务器上,方便日志的查看。springCloud本身提供了基于elk的日志采集,但是由于使用logstash,会加大运维成本。这里将使用轻量级的方案。

二、思路

我们的目的是提供轻量级的日志采集来代替logstash,日志最终还是会存进Elasticsearch。为了能轻量级的实现日志采集,并且避免对代码的侵入,我们可以扩展Logback的appender,也可以扩展log4j的appender。这样我们使用slf4j来记录日志的时候,日志自动会保存到Elasticsearch中,并且不用修改任何业务代码。

三、自定义Logback appender

我们先来看一下Logback的appender的Uml图,我们可以发现两个对我们有借鉴意义的类

  • UnsynchronizedAppenderBase提供了异步的日志记录

     

  • DBAppender基于数据库的日志记录

这两个类还是比较简单的,具体的代码我就不详细解说了,请自行查阅

属性注入

基本实现逻辑从UnsynchronizedAppenderBaseDBAppender已经能够知道了,现在把我们需要的信息注入到Appender中,这里需要如下的知识

Logback标签注入属性

我们可以直接在Xml中用标签配置属性,这些标签只要名称和appender中的成员变量名一致,则会自动把标签中的属性注入到成员变量中。

我们举一个例子:

xml这样配置

testdemotrue${CONSOLE_LOG_PATTERN_IDE}utf8

其中ElasticsearchAppender是我们自己实现的Appender。这里有一个profile标签,我们需要ElasticsearchAppender中成员变量的名称和该标签名一致,这样就可以把test值注入到成员变量profile中。

protected String profile = ""; // 运行环境

Spring配置信息注入属性

有些信息可能已经在spring中做了配置,我们不想要重复的配置,这个时候我们可以用springProperty标签来进行设置。

  • scope:作用范围

  • name:名称

  • source:spring配置

  • defaultValue:默认值,必须要指定

然后在标签中用上面的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);

获取属性值可以通过LoggingEventgetMDCPropertyMap方法先获取属性的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


推荐阅读
  • Spring Cloud因其强大的功能和灵活性,被誉为开发分布式系统的‘一站式’解决方案。它不仅简化了分布式系统中的常见模式实现,还被广泛应用于企业级生产环境中。本书内容详实,覆盖了从微服务基础到Spring Cloud的高级应用,适合各层次的开发者。 ... [详细]
  • Spring Cloud学习指南:深入理解微服务架构
    本文介绍了微服务架构的基本概念及其在Spring Cloud中的实现。讨论了微服务架构的主要优势,如简化开发和维护、快速启动、灵活的技术栈选择以及按需扩展的能力。同时,也探讨了微服务架构面临的挑战,包括较高的运维要求、分布式系统的复杂性、接口调整的成本等问题。最后,文章提出了实施微服务时应遵循的设计原则。 ... [详细]
  • 深入解析Spring Cloud微服务架构与分布式系统实战
    本文详细介绍了Spring Cloud在微服务架构和分布式系统中的应用,结合实际案例和最新技术,帮助读者全面掌握微服务的实现与优化。 ... [详细]
  • 前言无论是对于刚入行工作还是已经工作几年的java开发者来说,面试求职始终是你需要直面的一件事情。首先梳理自己的知识体系,针对性准备,会有事半功倍的效果。我们往往会把重点放在技术上 ... [详细]
  • 该平台旨在为大型企业提供一个高效、灵活且可扩展的分布式微服务架构解决方案。它采用模块化、微服务化和热部署的设计理念,结合当前最先进且无商业限制的主流开源技术,如Spring Cloud、Spring Boot2、MyBatis、OAuth2和Element UI,实现前后端分离的系统管理平台。 ... [详细]
  • 深入解析:阿里实战 SpringCloud 微服务架构与应用
    本文将详细介绍 SpringCloud 在微服务架构中的应用,涵盖入门、实战和案例分析。通过丰富的代码示例和实际项目经验,帮助读者全面掌握 SpringCloud 的核心技术和最佳实践。 ... [详细]
  • 探讨如何真正掌握Java EE,包括所需技能、工具和实践经验。资深软件教学总监李刚分享了对毕业生简历中常见问题的看法,并提供了详尽的标准。 ... [详细]
  • 本文探讨了如何在日常工作中通过优化效率和深入研究核心技术,将技术和知识转化为实际收益。文章结合个人经验,分享了提高工作效率、掌握高价值技能以及选择合适工作环境的方法,帮助读者更好地实现技术变现。 ... [详细]
  • 本文作者分享了在阿里巴巴获得实习offer的经历,包括五轮面试的详细内容和经验总结。其中四轮为技术面试,一轮为HR面试,涵盖了大量的Java技术和项目实践经验。 ... [详细]
  • 本文详细介绍了如何在 Android 中使用值动画(ValueAnimator)来动态调整 ImageView 的高度,并探讨了相关的关键属性和方法,包括图片填充后的高度、原始图片高度、动画变化因子以及布局重置等。 ... [详细]
  • 本文探讨了现代分布式架构的多样性,包括高并发、多活数据中心、容器化、微服务、高可用性和弹性架构等,并介绍了与这些架构相关的重要管理技术,如DevOps、应用监控和自动化运维。文章还深入分析了分布式系统的核心概念、主要用途及类型,同时对比了单体应用与分布式服务化的优缺点。 ... [详细]
  • 最新计算机专业原创毕业设计参考选题都有源码+数据库是近期作品ling取参考你的选题刚好在下面有,有时间看到机会给您发1ssm资源循环利用2springboot校园考勤系统3ssm防 ... [详细]
  • 本文探讨了Web开发与游戏开发之间的主要区别,旨在帮助开发者更好地理解两种开发领域的特性和需求。文章基于作者的实际经验和网络资料整理而成。 ... [详细]
  • SpringCloud电商平台开发指南:实战案例解析
    本文详细介绍了基于SpringCloud构建的电商平台项目,涵盖了从技术选型到项目部署的全流程,旨在帮助开发者快速掌握电商平台的开发技巧。 ... [详细]
  • 热璞数据库与云宏达成兼容性互认证,共筑数据安全屏障
    热璞数据库与云宏信息技术有限公司近期宣布完成产品兼容性互认证,旨在提升数据安全性与稳定性,支持企业数字化转型。 ... [详细]
author-avatar
live科_722
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有