在Spring Boot中用Loki实现日志记录


在本文中,您将了解如何收集 Spring Boot 应用程序日志并将其发送到Grafana Loki。我们将使用附加的Loki4j Logback。

Loki 是一个受 Prometheus 启发的水平可扩展、高可用的日志聚合系统。

本文展示如何配置应用程序和 Loki 之间的集成。但是,可以使用自动配置库来记录 HTTP 请求和响应,这将为您完成所有这些步骤。

克隆 GitHub 存储库

使用 Loki4j Logback Appender
为了使用Appender Loki4j Logback,我们需要在 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 文件:

<?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>


这样我们的Loki实例在http://localhost:3100 下可访问。

Loki并不对日志的内容进行索引--而只是对元数据标签进行索引。

  1. 有一些静态标签,如应用程序名称、日志级别或主机名。见上面logback-spring.xml第1行注释处
  2. 我们可以在format.label字段中设置它们(2)。
  3. 我们还将设置一些动态标签,因此我们启用Logback标记功能(3)。
  4. 最后,我们要设置日志格式模式(4)。为了简化,与LogQL(Loki查询语言)的潜在转换,我们将使用JSON符号。

除了静态标签,我们还可以发送动态数据:
例如,只针对当前请求的特定内容。假设我们有一个管理人的服务,我们想从请求中记录目标人的ID。

@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())); [b]// (1)[/b]
        LOG.info(marker, "Person successfully found"); [b]// (2)[/b]
        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");
    }

}

正如我之前提到的,使用Loki4j我们可以使用Logback标记。

在经典的Logback中,标记主要用于过滤日志记录。
使用Loki,我们只需要定义 LabelMarker 对象,其中包含动态字段的键/值图,代码在(1)处;然后我们将该对象传递给当前的日志行(2)。

假设我们在单个日志行中有多个动态字段,我们必须以这种方式创建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
在本地机器上运行Loki的最简单方法是使用Docker容器。除了Loki实例,我们还将运行Grafana来显示和搜索日志。下面是包含所有必要服务的docker-compose.yml。你可以用docker compose up命令来运行它们。

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>

运行:

$ mvn spring-boot:run

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

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

该库用几个默认标签记录每个请求和响应,包括例如requestId或correlationId。