51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Boot 应用中使用 Loki 记录日志

spring boot & Loki

在本文中,你将学习如何收集 Spring Boot 应用程序日志并将其发送到 Grafana Loki。为此,我们将使用 Loki4j Logback appender 功能。Loki 是一个受 Prometheus 启发的可水平扩展、高度可用的日志聚合系统。我将逐步展示如何配置应用程序与 Loki 之间的集成。不过,你也可以使用我自动配置的用于记录 HTTP 请求和响应的库,它将为你完成所有这些步骤。

源码 {#源码}

如果你想自己尝试,可以克隆我的 GitHub 仓库。点击 此处 查看包含我的自定义 Spring Boot 日志库的源代码仓库。然后按照我的说明操作即可。

使用 Loki4j Logback Appender {#使用-loki4j-logback-appender}

为了使用 Loki4j Logback Appender,我们需要在 Maven pom.xml 中加入一个依赖。该库的当前版本为 1.4.1

<dependency>
    <groupId>com.github.loki4j</groupId>
    <artifactId>loki-logback-appender</artifactId>
    <version>1.4.1</version>
</dependency>

然后,我们需要在 src/main/resources 目录下创建 logback-spring.xml 文件。我们的 Loki 实例在 http://localhost:3100 地址 (1) 下可用。Loki 不会索引日志内容,只会索引元数据标签。有一些静态标签,如应用程序名称、日志级别或主机名。我们可以在 format.label 字段 (2) 中设置它们。我们还将设置一些动态标签,因此要启用日志回溯标记功能 (3) 。最后,我们将设置日志格式模式 (4)。为了简化 LogQL(Loki 查询语言)的潜在转换,我们将使用 JSON 符号。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <springProperty name="name" source="spring.application.name" />

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>
        %d{HH:mm:ss.SSS} %-5level %logger{36} %X{X-Request-ID} - %msg%n
      </pattern>
    </encoder>
  </appender>

  <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
    <!-- (1) -->
    <http>
      <url>http://localhost:3100/loki/api/v1/push</url>
    </http>
    <format>
      <!-- (2) -->
      <label>
        <pattern>app=${name},host=${HOSTNAME},level=%level</pattern>
        <!-- (3) -->
        <readMarkers>true</readMarkers>
      </label>
      <message>
        <!-- (4) -->
        <pattern>
{
   "level":"%level",
   "class":"%logger{36}",
   "thread":"%thread",
   "message": "%message",
   "requestId": "%X{X-Request-ID}"
}
        </pattern>
      </message>
    </format>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="LOKI" />
  </root>

</configuration>

除了静态标签外,我们还可以发送动态数据,例如仅针对当前请求的特定数据。假设我们有一个管理人员的服务,我们希望记录请求中目标人员的 id。正如我之前提到的,使用 Loki4j,我们可以使用 Logback 标记来实现这一点。在经典的 Logback 中,标记主要用于过滤日志记录。使用 Loki,我们只需定义包含动态字段 key/value MapLabelMarker 对象 (1) 。然后将该对象传递给当前日志行 (2)

@RestController
@RequestMapping("/persons")
public class PersonController {

    private final Logger LOG = LoggerFactory
       .getLogger(PersonController.class);
    private final List<Person> persons = new ArrayList<>();

    @GetMapping
    public List<Person> findAll() {
        return persons;
    }

    @GetMapping("/{id}")
    public Person findById(@PathVariable("id") Long id) {
        Person p = persons.stream().filter(it -> it.getId().equals(id))
                .findFirst()
                .orElseThrow();
        LabelMarker marker = LabelMarker.of("personId", () -> 
           String.valueOf(p.getId())); // (1)
        LOG.info(marker, "Person successfully found"); // (2)
        return p;
    }

    @GetMapping("/name/{firstName}/{lastName}")
    public List<Person> findByName(
       @PathVariable("firstName") String firstName,
       @PathVariable("lastName") String lastName) {
       
       return persons.stream()
          .filter(it -> it.getFirstName().equals(firstName)
                        && it.getLastName().equals(lastName))
          .toList();
    }

    @PostMapping
    public Person add(@RequestBody Person p) {
        p.setId((long) (persons.size() + 1));
        LabelMarker marker = LabelMarker.of("personId", () -> 
           String.valueOf(p.getId()));
        LOG.info(marker, "New person successfully added");
        persons.add(p);
        return p;
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable("id") Long id) {
        Person p = persons.stream()
           .filter(it -> it.getId().equals(id))
           .findFirst()
           .orElseThrow();
        persons.remove(p);
        LabelMarker marker = LabelMarker.of("personId", () -> 
           String.valueOf(id));
        LOG.info(marker, "Person successfully removed");
    }

    @PutMapping
    public void update(@RequestBody Person p) {
        Person person = persons.stream()
                .filter(it -> it.getId().equals(p.getId()))
                .findFirst()
                .orElseThrow();
        persons.set(persons.indexOf(person), p);
        LabelMarker marker = LabelMarker.of("personId", () -> 
            String.valueOf(p.getId()));
        LOG.info(marker, "Person successfully updated");
    }

}

假设我们在单个日志行中有多个动态字段,我们就必须以这种方式创建 LabelMarker 对象:

LabelMarker marker = LabelMarker.of(() -> Map.of("audit", "true",
                    "X-Request-ID", MDC.get("X-Request-ID"),
                    "X-Correlation-ID", MDC.get("X-Correlation-ID")));

使用 Spring Boot 运行 Loki {#使用-spring-boot-运行-loki}

在本地计算机上运行 Loki 的最简单方法是使用 Docker 容器。除了 Loki 实例,我们还将运行 Grafana 来显示和搜索日志。下面是包含所有所需服务的 docker-compose.yml。你可以使用 docker compose up 命令来运行它们。不过,还有另一种方法 - 直接使用 Spring Boot 应用程序。

docker-compose.yml:

version: "3"

networks:
  loki:

services:
  loki:
    image: grafana/loki:2.8.2
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - loki

  grafana:
    environment:
      - GF_PATHS_PROVISIONING=/etc/grafana/provisioning
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
    entrypoint:
      - sh
      - -euc
      - |
        mkdir -p /etc/grafana/provisioning/datasources
        cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
        apiVersion: 1
        datasources:
        - name: Loki
          type: loki
          access: proxy
          orgId: 1
          url: http://loki:3100
          basicAuth: false
          isDefault: true
          version: 1
          editable: false
        EOF
        /run.sh        
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    networks:
      - loki

为了利用 Spring Boot Docker Compose 支持,我们需要将 docker-compose.yml 放在应用程序根目录下。然后,我们必须在 Maven pom.xml 中加入 Spring-boot-docker-compose 依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-docker-compose</artifactId>
  <optional>true</optional>
</dependency>

完成所有必要步骤后,我们就可以运行应用程序了。例如,使用以下 Maven 命令:

$ mvn spring-boot:run

此时,在应用程序运行之前,Spring Boot 会启动在 docker-compose.yml 中定义的容器。

spring-boot-loki-docker-compose

让我们只显示正在运行的容器列表。正如你所看到的,由于 Loki 监听的是本地端口 3100,所以一切正常:

$ docker ps            
CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS         PORTS                    NAMES
d23390fbee06   grafana/loki:2.8.2       "/usr/bin/loki -conf..."   4 minutes ago   Up 2 minutes   0.0.0.0:3100->3100/tcp   sample-spring-boot-web-loki-1
84a47637a50b   grafana/grafana:latest   "sh -euc 'mkdir -p /..."   2 days ago      Up 2 minutes   0.0.0.0:3000->3000/tcp   sample-spring-boot-web-grafana-1

测试 Spring Boot REST 应用的日志记录 {#测试-spring-boot-rest-应用的日志记录}

运行应用程序后,我们可以对 REST API 进行一些测试调用。首先,让我们添加一些 person:

$ curl 'http://localhost:8080/persons' \
  -H 'Content-Type: application/json' \
  -d '{"firstName": "AAA","lastName": "BBB","age": 20,"gender": "MALE"}'

$ curl 'http://localhost:8080/persons' \
  -H 'Content-Type: application/json' \
  -d '{"firstName": "CCC","lastName": "DDD","age": 30,"gender": "FEMALE"}'

$ curl 'http://localhost:8080/persons' \
  -H 'Content-Type: application/json' \
  -d '{"firstName": "EEE","lastName": "FFF","age": 40,"gender": "MALE"}'

然后,我们可以用不同的条件多次调用 "查询" 端点:

$ curl http://localhost:8080/persons/1
$ curl http://localhost:8080/persons/2
$ curl http://localhost:8080/persons/3

下面是控制台中的应用程序日志。只有简单的日志行,没有 JSON 格式。

简单的日志行

现在,让我们切换到 Grafana。我们已经配置了与 Loki 的集成。在新的仪表板中,我们需要选择 Loki。

spring-boot-loki-grafana-datasource

以下是存储在 Loki 中的应用程序日志的历史记录。

存储在 Loki 中的应用程序日志的历史记录

如你所见,我们使用 JSON 格式记录日志。某些日志行包含 Loki4j Logback appender 中的动态标签。

某些日志行包含 Loki4j Logback appender 中的动态标签

我们在一些日志中添加了 personId 标签,这样就可以轻松过滤只包含特定人员请求的记录。下面是过滤 personId=1 的记录的 LogQL 查询:

{app="first-service"} |= `` | personId = `1`

下面是 Grafana 面板上显示的结果:

Grafana 面板上显示的结果

我们还可以使用 LogQL 对日志进行格式化。由于采用了 JSON 格式,我们可以准备一个解析整个日志信息的查询。

{app="first-service"} |= `` | json

如你所见,现在 Loki 将所有 JSON 字段都视为元数据标签:

Loki 将所有 JSON 字段都视为元数据标签

使用 Spring Boot Loki Starter {#使用-spring-boot-loki-starter}

如果你不想自己配置这些东西,可以使用我的 Spring Boot 库,它提供了自动配置功能。此外,它还会自动记录所有传入的 HTTP 请求和传出的 HTTP 响应。如果默认设置已经足够,你只需将单个 Spring Boot starter 作为依赖项即可:

<dependency>
  <groupId>com.github.piomin</groupId>
  <artifactId>logstash-logging-spring-boot-starter</artifactId>
  <version>2.0.2</version>
</dependency>

该库用多个默认标签记录每个请求和响应,例如 requestIdcorrelationId

logstash-logging-spring-boot-starter


参考:https://piotrminkowski.com/2023/07/05/logging-in-spring-boot-with-loki/

赞(3)
未经允许不得转载:工具盒子 » 在 Spring Boot 应用中使用 Loki 记录日志