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


推荐阅读
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 2021年Java开发实战:当前时间戳转换方法详解与实用网址推荐
    在当前的就业市场中,金九银十过后,金三银四也即将到来。本文将分享一些实用的面试技巧和题目,特别是针对正在寻找新工作机会的Java开发者。作者在准备字节跳动的面试过程中积累了丰富的经验,并成功获得了Offer。文中详细介绍了如何将当前时间戳进行转换的方法,并推荐了一些实用的在线资源,帮助读者更好地应对技术面试。 ... [详细]
  • Hadoop平台警告解决:无法加载本机Hadoop库的全面应对方案
    本文探讨了在Hadoop平台上遇到“无法加载本机Hadoop库”警告的多种解决方案。首先,通过修改日志配置文件来忽略该警告,这一方法被证明是有效的。其次,尝试指定本地库的路径,但未能解决问题。接着,尝试不使用Hadoop本地库,同样没有效果。然后,通过替换现有的Hadoop本地库,成功解决了问题。最后,根据Hadoop的源代码自行编译本地库,也达到了预期的效果。以上方法适用于macOS系统。 ... [详细]
  • ### 优化后的摘要本学习指南旨在帮助读者全面掌握 Bootstrap 前端框架的核心知识点与实战技巧。内容涵盖基础入门、核心功能和高级应用。第一章通过一个简单的“Hello World”示例,介绍 Bootstrap 的基本用法和快速上手方法。第二章深入探讨 Bootstrap 与 JSP 集成的细节,揭示两者结合的优势和应用场景。第三章则进一步讲解 Bootstrap 的高级特性,如响应式设计和组件定制,为开发者提供全方位的技术支持。 ... [详细]
  • 如何正确配置与使用日志组件:Log4j、SLF4J及Logback的连接与整合方法
    在当前的软件开发实践中,无论是开源项目还是日常工作中,日志框架都是不可或缺的工具之一。本文详细探讨了如何正确配置与使用Log4j、SLF4J及Logback这三个流行的日志组件,并深入解析了它们之间的连接与整合方法,旨在帮助开发者高效地管理和优化日志记录流程。 ... [详细]
  • Mybatis_04日志
    前几天临近期末考试,一直在准备考试,吐槽一下,这个学期的考试真是全背书,服了,背吐了。考完试到元旦又放肆了几天 ... [详细]
  • 增加Maven构建profile配置在项目最顶层的pom.xml添加common和release两个profile,并声明${app.run.env}作为环境切换变量<profiles> ... [详细]
  • log4cpp概述与使用实例一、log4cpp概述Log4cpp是一个开源的C类库,它提供了C程序中使用日志和跟踪调试的功能,它的优点如下࿱ ... [详细]
  • 从0到1搭建大数据平台
    从0到1搭建大数据平台 ... [详细]
  • 本文详细介绍了 InfluxDB、collectd 和 Grafana 的安装与配置流程。首先,按照启动顺序依次安装并配置 InfluxDB、collectd 和 Grafana。InfluxDB 作为时序数据库,用于存储时间序列数据;collectd 负责数据的采集与传输;Grafana 则用于数据的可视化展示。文中提供了 collectd 的官方文档链接,便于用户参考和进一步了解其配置选项。通过本指南,读者可以轻松搭建一个高效的数据监控系统。 ... [详细]
  • 开发日志:高效图片压缩与上传技术解析 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 第二章:Kafka基础入门与核心概念解析
    本章节主要介绍了Kafka的基本概念及其核心特性。Kafka是一种分布式消息发布和订阅系统,以其卓越的性能和高吞吐量而著称。最初,Kafka被设计用于LinkedIn的活动流和运营数据处理,旨在高效地管理和传输大规模的数据流。这些数据主要包括用户活动记录、系统日志和其他实时信息。通过深入解析Kafka的设计原理和应用场景,读者将能够更好地理解其在现代大数据架构中的重要地位。 ... [详细]
  • CentOS 7环境下Jenkins的安装与前后端应用部署详解
    CentOS 7环境下Jenkins的安装与前后端应用部署详解 ... [详细]
  • 装饰模式(Deocrator)     动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。    所谓装饰,就是一些对象给主题 ... [详细]
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社区 版权所有