从0开始搭建SpringCloud项目分布式日志架构技术栈,绝了!

点击上方“Java基基”,选择“设为星标”

做积极的人,而不是积极废人!

每天 14:00 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:blog.csdn.net/guduyishuai/

article/details/81356000

ac6b01f0f260021888ff4f68ee22744d.jpeg


一、简介

SpringCloud 提供了自己的日志追踪,SpringCloud 提供了自己的上载日志记录,并提供了相应的日志记录。会使用轻量级的维成本。这里将使用级别的方案。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

二、思路

我们的目的是提供轻量级的日志收集,而不是 logstash,最终还是会存入 Elasticsearch。 log4j 的附加程序。这样我们使用 slf4j 来日志的时候,日志自动记录保存到 Elasticsearch 中,并且不用修改任何业务代码。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

三、自定义Logback appender

我们先看看 Logback 的 appender 的 Uml 图,我们可以发现两个我们有例子意义的类

7205d4aaf29690c54661244425bcc6ba.png
  • UnsynchronizedAppenderBase提供了异步的日志记录

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

类还是比较的,具体的我就不详细解释了,请自行简单两种

属性注入

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

Logback标签注入属性

我们可以直接在 Xml 中使用这些标签,只要名称appender中的成员名称一致,并且可以到中配置属性的标签,中的变量参数标签中。

举一个例子:

xml这样配置

<appender name="ES" class="com.luminroy.component.logger.appender.ElasticsearchAppender">
  <profile>test</profile>
  <esType>demo</esType>
  <withJansi>true</withJansi>
  <encoder>
   <pattern>${CONSOLE_LOG_PATTERN_IDE}</pattern>
   <charset>utf8</charset>
  </encoder>
 </appender>

ElasticsearchAppender这是我们自己实现的Appender。这里有一个配置文件,我们需要中ElasticsearchAppender成员变量的名称和该标签名称一致,这样就可以将测试值注入到成员配置文件中。

protected String profile = ""; // 运行环境
春天配置信息属性

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

  • 适用范围:作用范围

  • 名称: 名称

  • 来源:spring配置

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

然后在标签中用其名称属性作为位符,类中的成员变量名和标签名一致。

举一个例子:

xml这样配置

<springProperty scope="context" name="applicationName" source="spring.application.name"
     defaultValue=""/>
 <springProperty scope="context" name="profile" source="spring.profiles.active"
     defaultValue="default"/>
 
 <springProperty scope="context" name="esUserName" source="luminary.elasticsearch.username"
     defaultValue="elastic"/>
 
 <springProperty scope="context" name="esPassword" source="luminary.elasticsearch.password"
     defaultValue="123456"/>
 
 <springProperty scope="context" name="esServer" source="luminary.elasticsearch.server"
     defaultValue="127.0.0.1:9200"/>
 
 <springProperty scope="context" name="esMultiThreaded" source="luminary.elasticsearch.multiThreaded"
     defaultValue="true"/>
 
 <springProperty scope="context" name="esMaxTotalConnection" source="luminary.elasticsearch.maxTotalConnection"
     defaultValue="20"/>
 
 <springProperty scope="context" name="esMaxTotalConnectionPerRoute" source="luminary.elasticsearch.maxTotalConnectionPerRoute"
     defaultValue="5"/>
 
 <springProperty scope="context" name="esDiscoveryEnabled" source="luminary.elasticsearch.discoveryEnabled"
     defaultValue="true"/>
 
 <springProperty scope="context" name="esDiscorveryFrequency" source="luminary.elasticsearch.discorveryFrequency"
     defaultValue="60"/>
<appender name="ES" class="com.luminary.component.logger.appender.SpringElasticsearchAppender">
  <applicationName>${applicationName}</applicationName>
  <profile>${profile}</profile>
  <esType>demo</esType>
  <username>${esUserName}</username>
  <password>${esPassword}</password>
  <server>${esServer}</server>
  <multiThreaded>${esMultiThreaded}</multiThreaded>
  <maxTotalConnection>${esMaxTotalConnection}</maxTotalConnection>
  <maxTotalConnectionPerRoute>${esMaxTotalConnectionPerRoute}</maxTotalConnectionPerRoute>
  <discoveryEnabled>${esDiscoveryEnabled}</discoveryEnabled>
  <discorveryFrequency>${esDiscorveryFrequency}</discorveryFrequency>
 </appender>

yml这样配置

spring:
  application:
    name: logger-demo-server
 
luminary: 
  elasticsearch:
    username: elastic
    password: 123456
    server: 
      - 127.0.0.1:9200
    multiThreaded: true
    maxTotalConnection: 20
    maxTotalConnectionPerRoute: 5
    discoveryEnabled: true
    discorveryFrequency: 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代码注入属性

这里还有一种情况,有些属性需要在运行时才可以,或者时会改变。这需要能够动态注入属性。我们可以使用log4js的MDC类来解决。

cab7157ab2c5a2b4c6882ae25903f0dc.png

我们可以通过相应的放置,删除方法来动态设置属性。

例句:

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);

获取属性值可以通过先获取属性的map,再根据键名从map中获取LoggingEvent的方法来。getMDCPropertyMap

例句:

private String getRpcId(LoggingEvent event) {
    Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
 return mdcPropertyMap.get("rpcId");
}
 
private String getTraceId(LoggingEvent event) {
 Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
 return mdcPropertyMap.get("traceId");
}

值得说明的是,mdcAdapter 是一个人物的成员变量,但它本身是线程安全的,我们可以看一下logback 的实现

private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
        if (oldMap != null) {
            // we don't want the parent thread modifying oldMap while we are
            // iterating over it
            synchronized (oldMap) {
                newMap.putAll(oldMap);
            }
        }
 
        copyOnThreadLocal.set(newMap);
        return newMap;
    }
Elasticsearch模板设计

最后保存在 Elasticsearch 中,我们希望索引名为java-log-${date}的形式,类型名为实际的微服务名

最后我们对索引设置一个模板

举一个例子:

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<E> extends UnsynchronizedAppenderBase<E> implements LuminaryLoggerAppender<E> {
 
 private static final FastDateFormat SIMPLE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd");
 
 private static final FastDateFormat ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
 
 protected JestClient jestClient;
 
 private static final String CONFIG_PROPERTIES_NAME = "es.properties";
 
 // 可在xml中配置的属性
 @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 = ""; // 地址
 
 @Override
 public void start() {
  super.start();
  init();
 }
 
 @Override
 public void stop() {
  super.stop();
  // 关闭es客户端
  try {
   jestClient.close();
  } catch (IOException e) {
   addStatus(new ErrorStatus("close jestClient fail", this, e));
  }
 }
 
    @Override
    protected void append(E event) {
      if (!isStarted()) {
             return;
         }
 
      subAppend(event);
    }
 
    private void subAppend(E event) {
     if (!isStarted()) {
            return;
        }
     
     try {
            // this step avoids LBCLASSIC-139
            if (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 = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
        }
    }
    
    private void save(E event) {
     if(event instanceof LoggingEvent) {
      // 获得日志数据
   EsLogVO esLogVO = createData((LoggingEvent) event);
   // 保存到es中
   save(esLogVO);
     } else {
      addWarn("the error type of event!");
     }
    }
 
 private void save(EsLogVO esLogVO) {
  Gson gson = new Gson();
  String jsonString = gson.toString();
 
  String esIndexFormat = esIndex.replace("#date#", SIMPLE_FORMAT.format(Calendar.getInstance().getTime()));
  Index index = new Index.Builder(esLogVO).index(esIndexFormat).type(esType).build();
 
  try {
   DocumentResult result = jestClient.execute(index);
   addStatus(new InfoStatus("es logger result:"+result.getJsonString(), this));
  } catch (Exception e) {
   addStatus(new ErrorStatus("jestClient exec fail", this, e));
  }
 }
 
 private EsLogVO createData(LoggingEvent event) {
  EsLogVO esLogVO = new EsLogVO();
 
  // 获得applicationName
  esLogVO.setApplicationName(applicationName);
  
  // 获得profile
  esLogVO.setProfile(profile);
  
  // 获得ip
  esLogVO.setIp(HostUtil.getIP());
 
  // 获得hostName
  esLogVO.setHost(HostUtil.getHostName());
 
  // 获得时间
  long dateTime = getDateTime(event);
  esLogVO.setDateTime(ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS.format(Calendar.getInstance().getTime()));
 
  // 获得线程
  String threadName = getThead(event);
  esLogVO.setThread(threadName);
 
  // 获得日志等级
  String level = getLevel(event);
  esLogVO.setLevel(level);
 
  // 获得调用信息
  EsLogVO.Location location = getLocation(event);
  esLogVO.setLocation(location);
 
  // 获得日志信息
  String message = getMessage(event);
  esLogVO.setMessage(message);
 
  // 获得异常信息
  String throwable = getThrowable(event);
  esLogVO.setThrowable(throwable);
 
  // 获得traceId
  String traceId = getTraceId(event);
  esLogVO.setTraceId(traceId);
 
  // 获得rpcId
  String rpcId = getRpcId(event);
  esLogVO.setRpcId(rpcId);
 
  return esLogVO;
 }
 
 private String getRpcId(LoggingEvent event) {
  Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
  return mdcPropertyMap.get("rpcId");
 }
 
 private String getTraceId(LoggingEvent event) {
  Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
  return mdcPropertyMap.get("traceId");
 }
 
 private String getThrowable(LoggingEvent event) {
  String exceptionStack = "";
  IThrowableProxy tp = event.getThrowableProxy();
  if (tp == null)
   return "";
 
  StringBuilder sb = new StringBuilder(2048);
  while (tp != null) {
 
   StackTraceElementProxy[] stackArray = tp.getStackTraceElementProxyArray();
 
   ThrowableProxyUtil.subjoinFirstLine(sb, tp);
 
   int commonFrames = tp.getCommonFrames();
   StackTraceElementProxy[] stepArray = tp.getStackTraceElementProxyArray();
   for (int i = 0; i < stepArray.length - commonFrames; i++) {
    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 = tp.getCause();
  }
  return sb.toString();
 }
 
 private String getMessage(LoggingEvent event) {
  return event.getFormattedMessage();
 }
 
 private EsLogVO.Location getLocation(LoggingEvent event) {
  EsLogVO.Location location = new EsLogVO.Location();
  if(isLocationInfo) {
   StackTraceElement[] cda = event.getCallerData();
   if (cda != null && cda.length > 0) {
    StackTraceElement immediateCallerData = 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 = ElasticsearchAppender.class.getClassLoader();
   Set<URL> esConfigPathSet = new LinkedHashSet<URL>();
   Enumeration<URL> paths;
   if (esClassLoader == null) {
    paths = ClassLoader.getSystemResources(CONFIG_PROPERTIES_NAME);
   } else {
    paths = esClassLoader.getResources(CONFIG_PROPERTIES_NAME);
   }
   while (paths.hasMoreElements()) {
    URL path = paths.nextElement();
    esConfigPathSet.add(path);
   }
 
   if(esConfigPathSet.size() == 0) {
    subInit();
    if(jestClient == null) {
     addWarn("没有获取到配置信息!");
     // 用默认信息初始化es客户端
     jestClient = new JestClientMgr().getJestClient();
    }
   } else {
 
    if (esConfigPathSet.size() > 1) {
     addWarn("获取到多个配置信息,将以第一个为准!");
    }
 
    URL path = esConfigPathSet.iterator().next();
    try {
     Properties config = new Properties();
     @Cleanup InputStream input = new FileInputStream(path.getPath());
     config.load(input);
     // 通过properties初始化es客户端
     jestClient = new JestClientMgr(config).getJestClient();
    } catch (Exception e) {
     addStatus(new ErrorStatus("config fail", this, e));
    }
 
   }
  } catch (Exception e) {
   addStatus(new ErrorStatus("config fail", this, e));
  }
 }
 
 @Override
 public void subInit() {
  // template method
 }
    
}
代码地址:

https://github.com/wulinfeng2/luminary-component



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

c09a8ef23d348315f0760abd23e236bd.png

已在知识星球更新源码解析如下:

6bcb9145dd128921bd16c52e1b94110f.jpeg

f9070c8d8b2858a5f69c4e5440adc437.jpeg

a1ced0cec63cd26228c0fc9ba99e710f.jpeg

620c31c77107be43928cb68329e64b17.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值