Spring Boot 使用 Grafana Loki 来收集和显示日志

1、简介

Grafana 实验室受 Prometheus 的启发开发了开源日志聚合系统 Loki。该系统的目的是存储日志数据并编制索引,从而方便高效地查询和分析由不同应用和系统生成的日志。

本文将带你了解如何在 Spring Boot 中使用 Loki 收集和汇总日志,并使用 Grafana 显示日志。

2、运行 Loki 和 Grafana 服务

首先以 Docker 容器的方式启动 Loki 和 Grafana 服务,以便收集和观察日志。

docker-compose 文件中定义 LokiGrafana 服务:

version: "3"
networks:
  loki:
services:
  loki:
    image: grafana/loki:2.9.0
    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

接着,使用 docker-compose 命令来启动服务:

docker-compose up

最后,确认两个服务是否都已启动:

docker ps

211c526ea384        grafana/loki:2.9.0       "/usr/bin/loki -conf…"   4 days ago          Up 56 seconds       0.0.0.0:3100->3100/tcp   surajmishra_loki_1
a1b3b4a4995f        grafana/grafana:latest   "sh -euc 'mkdir -p /…"   4 days ago          Up 56 seconds       0.0.0.0:3000->3000/tcp   surajmishra_grafana_1

3、Spring Boot 配置 Loki

启动 Grafana 和 Loki 服务后,需要配置应用,以便向其发送日志。

我们使用 loki-logback-appender 来负责向 Loki Aggregator 发送日志,以存储日志并编制索引。

首先,在 pom.xml 中添加 loki-logback-appender 依赖:

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

其次,需要在 src/main/resources 文件夹下创建 logback-spring.xml 文件。该文件将控制 Spring Boot 应用的日志记录行为,如日志格式、Loki 服务的端点等:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>        
   <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
        <http>
            <url>http://localhost:3100/loki/api/v1/push</url>
        </http>
        <format>
            <label>
                <pattern>app=${name},host=${HOSTNAME},level=%level</pattern>
                <readMarkers>true</readMarkers>
            </label>
            <message>
                <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="LOKI" />
     </root>
</configuration>

配置完成后,编写一个简单的服务,记录 INFO 级别的数据:

@Service
class DemoService{

    private final Logger LOG = LoggerFactory.getLogger(DemoService.class);

    public void log(){
        LOG.info("DemoService.log invoked");
    }
}

4、测试

通过启动 Grafana 和 Loki 容器,执行 Service 方法将日志推送到 Loki 来进行一次实时测试。之后,使用 HTTP API 查询 Loki,以确认日志确实被推送。关于启动 Grafana 和 Loki 容器,请参阅前面的章节。

首先,执行 DemoService.log() 方法,该方法将调用 Logger.info(),这将使用 loki-logback-appender 发送一条消息,并会被 Loki 收集:

DemoService service = new DemoService();
service.log();

接着,创建一个请求,用于调用 Loki HTTP API 提供的 REST 端点。该 GET API 接受代表查询、开始时间和结束时间的查询参数。

把这些参数添加到请求对象中:

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

String query = "{level=\"INFO\"} |= `DemoService.log invoked`";

// 获取 UTC 时间
LocalDateTime currentDateTime = LocalDateTime.now(ZoneOffset.UTC);
String current_time_utc = currentDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));

LocalDateTime tenMinsAgo = currentDateTime.minusMinutes(10);
String start_time_utc = tenMinsAgo.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));

URI uri = UriComponentsBuilder.fromUriString(baseUrl)
  .queryParam("query", query)
  .queryParam("start", start_time_utc)
  .queryParam("end", current_time_utc)
  .build()
  .toUri();

接着,使用 restTemplate 对象来执行 REST 请求:

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(headers), String.class);

现在,需要处理响应并提取我们感兴趣的日志信息,使用 ObjectMapper 读取 JSON 响应并提取日志信息:

ObjectMapper objectMapper = new ObjectMapper();
List<String> messages = new ArrayList<>();
String responseBody = response.getBody();
JsonNode jsonNode = objectMapper.readTree(responseBody);
JsonNode result = jsonNode.get("data")
  .get("result")
  .get(0)
  .get("values");

result.iterator()
  .forEachRemaining(e -> {
      Iterator<JsonNode> elements = e.elements();
      elements.forEachRemaining(f -> messages.add(f.toString()));
  });

最后,断言在响应中收到的消息包含 DemoService 所记录的消息:

assertThat(messages).anyMatch(e -> e.contains(expected));

5、日志聚合和可视化

配置了 loki-logback-appender 后,我们的服务日志被推送到了 Loki 服务。

可以通过浏览器访问 http://localhost:3000(Grafana 服务)来查看。

要查看在 Loki 中存储和索引的日志,需要使用 Grafana。Grafana 数据源为 Loki 提供了可配置的连接参数,我们需要在其中输入 Loki 端点、身份验证机制等信息。

首先,配置 Loki 端点:

Grafana 配置 Loki 端点

成功配置数据源后,就可以进入数据页面查询日志了:

Loki 日志设置

我们可以编写查询(query),将应用日志提取到 Grafana 中进行可视化。

在我们的演示服务中,推送的是 INFO 日志,因此可以将其添加到过滤器中并运行查询:

Grafana 查询日志面板

运行查询后,可以看到所有符合搜索条件的 INFO 日志:

Loki 查询结果

6、总结

本文通过示例介绍了如何在 Spring Boot 中使用 Loki 记录应用日志,并通过 Grafana 控制台查询已记录的日志信息。


Ref:https://www.baeldung.com/spring-boot-loki-grafana-logging