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


推荐阅读
  • 本文详细介绍了Java代码分层的基本概念和常见分层模式,特别是MVC模式。同时探讨了不同项目需求下的分层策略,帮助读者更好地理解和应用Java分层思想。 ... [详细]
  • 对象存储与块存储、文件存储等对比
    看到一篇文档,讲对象存储,好奇,搜索文章,摘抄,学习记录!背景:传统存储在面对海量非结构化数据时,在存储、分享与容灾上面临很大的挑战,主要表现在以下几个方面:传统存储并非为非结 ... [详细]
  • 软件测试行业深度解析:迈向高薪的必经之路
    本文深入探讨了软件测试行业的发展现状及未来趋势,旨在帮助有志于在该领域取得高薪的技术人员明确职业方向和发展路径。 ... [详细]
  • Redis:缓存与内存数据库详解
    本文介绍了数据库的基本分类,重点探讨了关系型与非关系型数据库的区别,并详细解析了Redis作为非关系型数据库的特点、工作模式、优点及持久化机制。 ... [详细]
  • 8个IDC大数据基础定义解析丨IDC
    本文针对IDC数据行业相关名词术语进行解析,分为4组相关概念,希望大家读完 ... [详细]
  • Java EE 平台集成了多种服务、API 和协议,旨在支持基于 Web 的多层应用程序开发。本文将详细介绍 Java EE 中的 13 种关键技术规范,帮助开发者更好地理解和应用这些技术。 ... [详细]
  • 本文探讨了在一个物理隔离的环境中构建数据交换平台所面临的挑战,包括但不限于数据加密、传输监控及确保文件交换的安全性和可靠性。同时,作者结合自身项目经验,分享了项目规划、实施过程中的关键决策及其背后的思考。 ... [详细]
  • 流处理中的计数挑战与解决方案
    本文探讨了在流处理中进行计数的各种技术和挑战,并基于作者在2016年圣何塞举行的Hadoop World大会上的演讲进行了深入分析。文章不仅介绍了传统批处理和Lambda架构的局限性,还详细探讨了流处理架构的优势及其在现代大数据应用中的重要作用。 ... [详细]
  • 深入理解云计算与大数据技术
    本文详细探讨了云计算与大数据技术的关键知识点,包括大数据处理平台、社会网络大数据、城市大数据、工业大数据、教育大数据、数据开放与共享的应用,以及搜索引擎与Web挖掘、推荐技术的研究及应用。文章还涵盖了云计算的基础概念、特点和服务类型分类。 ... [详细]
  • 深入探讨:Actor模型如何解决并发与分布式计算难题
    在现代软件开发中,高并发和分布式系统的设计面临着诸多挑战。本文基于Akka最新文档,详细探讨了Actor模型如何有效地解决这些挑战,并提供了对并发和分布式计算的新视角。 ... [详细]
  • PGXC中的两阶段提交机制及其对事务一致性的保障
    PGXC作为一款基于PostgreSQL的分布式数据库系统,利用Sharding技术将数据分散存储于多个数据库实例中。本文探讨了PGXC的两阶段提交过程及其实现事务强一致性的方法。 ... [详细]
  • 深入解析:存储技术的演变与发展
    本文探讨了从单机文件系统到分布式文件系统的存储技术发展过程,详细解释了各种存储模型及其特点。 ... [详细]
  • 协程作为一种并发设计模式,能有效简化Android平台上的异步代码处理。自Kotlin 1.3版本引入协程以来,这一特性基于其他语言的成熟理念,为开发者提供了新的工具,以增强应用的响应性和效率。 ... [详细]
  • oracle 对硬件环境要求,Oracle 10G数据库软硬件环境的要求 ... [详细]
  • 近年来,区块链技术备受关注,其中比特币(Bitcoin)功不可没。尽管数字货币的概念早在上个世纪就被提出,但直到比特币的诞生,这一概念才真正落地生根。本文将详细探讨比特币、以太坊和超级账本(Hyperledger)的核心技术和应用场景。 ... [详细]
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社区 版权所有