Spring Cloud - 使用 ELK 和 Sleuth 的分布式日志记录

  • 介绍

    在分布式环境或单体环境中,应用程序日志对于出现问题时的调试非常重要。在本节中,我们将研究如何有效地记录日志并提高可追溯性,以便我们可以轻松查看日志。
    日志记录模式对日志记录至关重要的两个主要原因 -
    • Inter-service calls− 在微服务架构中,我们有服务之间的异步和同步调用。链接这些请求非常重要,因为单个请求可能有多个嵌套级别。
    • Intra-service calls− 单个服务收到多个请求,并且它们的日志很容易混合在一起。这就是为什么拥有一些与请求相关联的 ID 对于过滤请求的所有日志变得很重要。
    Sleuth 是用于登录应用程序的著名工具,ELK 用于更简单地观察整个系统。
  • 依赖设置

    让我们使用我们在每章中都使用过的 Restaurant 案例。因此,假设我们的客户服务和餐厅服务通过 API 进行通信,即同步通信。我们希望使用 Sleuth 来跟踪请求,并使用 ELK 堆栈进行集中可视化。
    为此,首先设置 ELK 堆栈。为此,首先,我们将设置 ELK 堆栈。我们将使用 Docker 容器启动 ELK 堆栈。以下是我们可以考虑的图像 -
    执行 ELK 设置后,通过点击以下 API 确保它按预期工作 -
    • Elasticsearch - 本地主机:9200
    • Kibana - 本地主机:5601
    我们将在本节末尾查看 logstash 配置文件。
    然后,让我们将以下依赖项添加到我们的客户服务和餐厅服务 -
    
    
    <dependency>
    
          <groupId>org.springframework.cloud</groupId>
    
          <artifactId>spring-cloud-starter-sleuth</artifactId>
    
    </dependency>
    
    
    现在我们已经设置了依赖项并运行了 ELK,让我们转到核心示例。
  • 服务内部请求跟踪

    在非常基本的层面上,以下是 Sleuth 添加的元数据 -
    • Service name − 当前正在处理请求的服务。
    • Trace Id− 将元数据 ID 添加到跨服务发送的日志中,以处理输入请求。这对于将处理一个输入请求的所有内部请求进行分组的服务间通信很有用。
    • Span Id− 将元数据 ID 添加到日志中,该 ID 与服务记录的所有日志语句相同,用于处理请求。它对服务内日志很有用。注意Span ID = Trace Id 对于父服务。
    让我们看看这在行动中。为此,让我们更新我们的客户服务代码以包含日志行。这是我们将使用的控制器代码。
    
    
    package com.jc2182;
    
    import java.util.HashMap;
    
    import org.slf4j.Logger;
    
    import org.slf4j.LoggerFactory;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import org.springframework.messaging.Message;
    
    import org.springframework.web.bind.annotation.PathVariable;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    
    class RestaurantCustomerInstancesController {
    
       Logger logger =
    
    LoggerFactory.getLogger(RestaurantCustomerInstancesController.class);
    
       static HashMap<Long, Customer> mockCustomerData = new HashMap();
    
       static{
    
          mockCustomerData.put(1L, new Customer(1, "Jane", "DC"));
    
          mockCustomerData.put(2L, new Customer(2, "John", "SFO"));
    
          mockCustomerData.put(3L, new Customer(3, "Kate", "NY"));
    
       }
    
       @RequestMapping("/customer/{id}")
    
       public Customer getCustomerInfo(@PathVariable("id") Long id) {
    
          logger.info("Querying customer with id: " + id);
    
          Customer customer = mockCustomerData.get(id);
    
          if(customer != null) {
    
             logger.info("Found Customer: " + customer);
    
          }
    
          return customer;
    
       }
    
    }
    
    
    现在让我们像往常一样执行代码,启动 Eureka Server。请注意,这不是硬性要求,为了完整起见,在此列出。
    然后,让我们使用以下命令编译并开始更新客户服务 -
    
    
    mvn clean install ; java -Dapp_port=8083 -jar .\target\spring-cloud-eurekaclient-
    
    1.0.jar
    
    
    我们准备好了,现在让我们通过点击 API 来测试我们的代码片段 -
    
    
    curl -X GET http://localhost:8083/customer/1
    
    
    这是我们将为此 API 获得的输出 -
    
    
    {
    
       "id": 1,
    
       "name": "Jane",
    
       "city": "DC"
    
    }
    
    
    现在让我们检查客户服务的日志 -
    
    
    2021-03-23 13:46:59.604 INFO [customerservice,
    
    b63d4d0c733cc675,b63d4d0c733cc675] 11860 --- [nio-8083-exec-7]
    
    .t.RestaurantCustomerInstancesController : Querying customer with id: 1
    
    2021-03-23 13:46:59.605 INFO [customerservice,
    
    b63d4d0c733cc675,b63d4d0c733cc675] 11860 --- [nio-8083-exec-7]
    
    .t.RestaurantCustomerInstancesController : Found Customer: Customer [id=1,
    
    name=Jane, city=DC]
    
    …..
    
    
    因此,正如我们所见,我们将服务名称、跟踪 ID 和跨度 ID 添加到我们的日志语句中。
  • 跨服务请求跟踪

    让我们看看如何跨服务进行日志记录和跟踪。因此,例如,我们要做的是使用内部调用客户服务的餐厅服务。
    为此,让我们更新我们的餐厅服务代码以包含日志行。这是我们将使用的控制器代码。
    
    
    package com.jc2182;
    
    import java.util.HashMap;
    
    import java.util.List;
    
    import java.util.function.Consumer;
    
    import java.util.function.Supplier;
    
    import java.util.stream.Collectors;
    
    import org.slf4j.Logger;
    
    import org.slf4j.LoggerFactory;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import org.springframework.context.annotation.Bean;
    
    import org.springframework.web.bind.annotation.PathVariable;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    
    class RestaurantController {
    
       @Autowired
    
       CustomerService customerService;
    
       Logger logger = LoggerFactory.getLogger(RestaurantController.class);
    
       static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
    
       static{
    
          mockRestaurantData.put(1L, new Restaurant(1, "Pandas", "DC"));
    
          mockRestaurantData.put(2L, new Restaurant(2, "Indies", "SFO"));
    
          mockRestaurantData.put(3L, new Restaurant(3, "Little Italy", "DC"));
    
          mockRestaurantData.put(4L, new Restaurant(4, "Pizeeria", "NY"));
    
       }
    
       @RequestMapping("/restaurant/customer/{id}")
    
       public List<Restaurant> getRestaurantForCustomer(@PathVariable("id") Long id) {
    
          logger.info("Get Customer from Customer Service with customer id: " + id);
    
          Customer customer = customerService.getCustomerById(id);
    
          logger.info("Found following customer: " + customer);
    
          String customerCity = customer.getCity();
    
          return mockRestaurantData.entrySet().stream().filter(
    
          entry -> entry.getValue().getCity().equals(customerCity))
    
          .map(entry -> entry.getValue())
    
          .collect(Collectors.toList());
    
       }
    
    }
    
    
    让我们使用以下命令编译并开始更新餐厅服务 -
    
    
    mvn clean install; java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
    
    
    确保我们有 Eureka 服务器和客户服务正在运行。我们准备好了,现在让我们通过点击 API 来测试我们的代码片段 -
    
    
    curl -X GET http://localhost:8082/restaurant/customer/2
    
    
    这是我们将为此 API 获得的输出 -
    
    
    [
    
       {
    
          "id": 2,
    
          "name": "Indies",
    
          "city": "SFO"
    
       }
    
    ]
    
    
    现在,让我们检查餐厅服务的日志 -
    
    
    2021-03-23 14:44:29.381 INFO [restaurantservice,
    
    6e0c5b2a4fc533f8,6e0c5b2a4fc533f8] 19600 --- [nio-8082-exec-6]
    
    com.jc2182.RestaurantController : Get Customer from Customer Service
    
    with customer id: 2
    
    2021-03-23 14:44:29.400 INFO [restaurantservice,
    
    6e0c5b2a4fc533f8,6e0c5b2a4fc533f8] 19600 --- [nio-8082-exec-6]
    
    com.jc2182.RestaurantController : Found following customer: Customer
    
    [id=2, name=John, city=SFO]
    
    
    然后,让我们检查客户服务的日志 -
    
    
    2021-03-23 14:44:29.392 INFO [customerservice,
    
    6e0c5b2a4fc533f8,f2806826ac76d816] 11860 --- [io-8083-exec-10]
    
    .t.RestaurantCustomerInstancesController : Querying customer with id: 2
    
    2021-03-23 14:44:29.392 INFO [customerservice,
    
    6e0c5b2a4fc533f8,f2806826ac76d816] 11860 --- [io-8083-exec-10]
    
    .t.RestaurantCustomerInstancesController : Found Customer: Customer [id=2,
    
    name=John, city=SFO]…..
    
    
    因此,正如我们所见,我们将服务名称、跟踪 ID 和跨度 ID 添加到我们的日志语句中。另外,我们看到跟踪 ID,即 6e0c5b2a4fc533f8 在客户服务和餐厅服务中重复。
  • 使用 ELK 进行集中日志记录

    到目前为止,我们看到的是一种通过 Sleuth 改进我们的日志记录和跟踪能力的方法。然而,在微服务架构中,我们有多个服务运行,每个服务运行多个实例。查看每个实例的日志来识别请求流是不切实际的。这就是 ELK 帮助我们的地方。
    让我们使用与 Sleuth 相同的服务间通信案例。让我们更新我们的餐厅和客户以添加logback appenders 对于 ELK 堆栈。
    在继续之前,请确保已设置 ELK 堆栈并且 Kibana 可在 localhost:5601 上访问。此外,使用以下设置配置 Lostash 配置 -
    
    
    input {
    
       tcp {
    
          port => 8089
    
          codec => json
    
       }
    
    }
    
    output {
    
       elasticsearch {
    
          index => "restaurant"
    
          hosts => ["http://localhost:9200"]
    
       }
    
    }
    
    
    完成此操作后,我们需要执行两个步骤才能在 Spring 应用程序中使用 logstash。我们将为我们的服务执行以下步骤。首先,为 logback 添加一个依赖项,以便为 Logstash 使用 appender。
    
    
    <dependency>
    
    <groupId>net.logstash.logback</groupId>
    
       <artifactId>logstash-logback-encoder</artifactId>
    
       <version>6.6</version>
    
    </dependency>
    
    
    其次,为 logback 添加一个 appender,以便 logback 可以使用这个 appender 将数据发送到 Logstash
    
    
    <?xml version="1.0" encoding="UTF-8"?>
    
    <configuration>
    
       <appender name="logStash"
    
    class="net.logstash.logback.appender.LogstashTcpSocketAppender">
    
          <destination>10.24.220.239:8089</destination>
    
          <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
    
       </appender>
    
       <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    
          <encoder>
    
             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -
    
    %msg%n</pattern>
    
          </encoder>
    
       </appender>
    
       <root level="INFO">
    
          <appender-ref ref="logStash" />
    
          <appender-ref ref="console" />
    
       </root>
    
    </configuration>
    
    
    以上 appender将登录到控制台并将日志发送到logstash。现在已经完成了,我们都准备测试一下。
    现在,让我们像往常一样执行上面的代码,启动 Eureka Server。
    然后,让我们使用以下命令编译并开始更新客户服务 -
    
    
    mvn clean install ; java -Dapp_port=8083 -jar .\target\spring-cloud-eurekaclient- 1.0.jar
    
    
    然后,让我们使用以下命令编译并开始更新餐厅服务 -
    
    
    mvn clean install; java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client- 1.0.jar
    
    
    我们准备好了,现在让我们通过点击 API 来测试我们的代码片段 -
    
    
    curl -X GET http://localhost:8082/restaurant/customer/2
    
    
    这是我们将为此 API 获得的输出 -
    
    
    [
    
       {
    
          "id": 2,
    
          "name": "Indies",
    
          "city": "SFO"
    
       }
    
    ]
    
    
    但更重要的是,日志语句也将在 Kibana 上可用。
    日志语句
    所以,如我们所见,我们可以过滤一个 traceId 并查看为满足请求而记录的跨服务的所有日志语句。