热门标签 | 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


推荐阅读
  • 此版本重点升级了Online代码生成器,支持更多的控件生成,所见即所得,极大的提高开发效率;同时做了数据库兼容专项工作,让Online开发兼容更多数据库:Mysql、SqlServer、Oracle、Postgresql等!!!项目介绍 ... [详细]
  • 一次上线事故,30岁+的程序员踩坑经验之谈
    本文主要介绍了一位30岁+的程序员在一次上线事故中踩坑的经验之谈。文章提到了在双十一活动期间,作为一个在线医疗项目,他们进行了优惠折扣活动的升级改造。然而,在上线前的最后一天,由于大量数据请求,导致部分接口出现问题。作者通过部署两台opentsdb来解决问题,但读数据的opentsdb仍然经常假死。作者只能查询最近24小时的数据。这次事故给他带来了很多教训和经验。 ... [详细]
  • Spring入门第十讲——Hibernate5.0.7+Struts2.3.24+Spring4.2.4三大框架整合开发
    回顾SSH框架至此,Hibernate-5.0.7、Struts-2.3.24和Spring-4.2.4这三个框架,我们已经都过了一遍了。现在,咱就要 ... [详细]
  • 基本信息|根源_SpringBoot 监控统计:SQL监控慢SQL记录Spring监控去广告
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了SpringBoot监控统计:SQL监控慢SQL记录Spring监控去广告相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 技能速成|一文带你学会MybatisPlus
    一.MP简介我们知道,Mybatis属于一个半自动的ORM框架。之所以说Mybatis是一个半自动的ORM框架,原因是它还需要我们自己在注解或是映射文 ... [详细]
  • 利用Dubbo的SPI扩展容器
    在实际开发中,很多Dubbo的服务提供者都会运行在web容器上,如果提供者服务上同时对外 ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 2018深入java目标计划及学习内容
    本文介绍了作者在2018年的深入java目标计划,包括学习计划和工作中要用到的内容。作者计划学习的内容包括kafka、zookeeper、hbase、hdoop、spark、elasticsearch、solr、spring cloud、mysql、mybatis等。其中,作者对jvm的学习有一定了解,并计划通读《jvm》一书。此外,作者还提到了《HotSpot实战》和《高性能MySQL》等书籍。 ... [详细]
  • Unit4博客&课程总结Unit4作业的架构设计本单元作业的设计我分为了三个模块处理:模型构建+预处理+任务函数,前两部分即为整个图的完整构建,第三部分即为实现题目要求的查询方法。 ... [详细]
  • 本文主要介绍关于uml,java,开发语言的知识点,对【【UML建模案例】小型网上书店系统】和【uml建模线上授课】有兴趣的朋友可以看下由【明天的土拨鼠】投稿的技术文章,希望该技术和经验能帮到你解决你 ... [详细]
  • ELK stack 学习记录
    ELK日志分析平台学习记录首先ELK主要指elasticsearch、logstash和kibana,三个开源软件组合而成的一套日志平台解决方案。可以将平时收集到的日志,通过前台展 ... [详细]
  • springboot日志【转】【补】
     市面上的日志框架日志门面(日志的抽象层)日志实现JCL(JakartaCommonsLogging)(2014)SLF4j(SimpleLoggingFacadeforJava) ... [详细]
  • java日志框架详解
    Java日志框架详解1.常用日志框架1.1Java常用日志框架类别1.2Java常用日志框架历史1.3两大日志接口阵营1.3.1基于CommonsLogging接口实现的常用日志框 ... [详细]
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社区 版权所有