在本文中,你将学习如何收集 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 Map
的 LabelMarker
对象 (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
中定义的容器。
让我们只显示正在运行的容器列表。正如你所看到的,由于 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。
以下是存储在 Loki 中的应用程序日志的历史记录。
如你所见,我们使用 JSON 格式记录日志。某些日志行包含 Loki4j Logback appender 中的动态标签。
我们在一些日志中添加了 personId
标签,这样就可以轻松过滤只包含特定人员请求的记录。下面是过滤 personId=1
的记录的 LogQL 查询:
{app="first-service"} |= `` | personId = `1`
下面是 Grafana 面板上显示的结果:
我们还可以使用 LogQL 对日志进行格式化。由于采用了 JSON 格式,我们可以准备一个解析整个日志信息的查询。
{app="first-service"} |= `` | 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>
该库用多个默认标签记录每个请求和响应,例如 requestId
或 correlationId
。
参考:https://piotrminkowski.com/2023/07/05/logging-in-spring-boot-with-loki/