我最近的一次演讲主要聚焦于可观察性的一般概念以及分布式追踪的特殊应用,特别是通过OpenTelemetry实现。在演示中,我展示了如何查看一个简单分布式系统的追踪,该系统由Apache APISIX API网关、使用Spring Boot的Kotlin应用、使用Flask的Python应用以及使用Axum的Rust应用组成。
今年早些时候,我在FOSDEM的可观察性分会场进行了演讲并参与了讨论。其中一场演讲展示了Grafana堆栈的演示:Mimir用于指标,Tempo用于追踪,Loki用于日志。我惊喜地发现,从一个工具切换到另一个工具是多么流畅。因此,我想要在我的演示中实现同样的效果,但通过OpenTelemetry来避免与Grafana堆栈的紧密耦合。
在这篇博客文章中,我将重点讨论日志和Loki。
Loki基础与我们的第一个程序
Loki的核心是一个日志存储引擎:
Loki是一个受Prometheus启发的水平扩展、高可用、多租户日志聚合系统。它的设计非常经济高效且易于操作。它不索引日志的内容,而是为每个日志流设置一组标签。
Loki提供了一个RESTful API来存储和读取日志。让我们从一个Java应用推送一条日志。Loki期望以下载荷结构:
I’ll use Java, but you can achieve the same result with a different stack. The most straightforward code is the following:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
var template = "'{' \"streams\": ['{' \"stream\": '{' \"app\": \"{0}\" '}', \"values\": [[ \"{1}\", \"{2}\" ]]'}']'}'"; //1
var now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
var nowInEpochNanos = NANOSECONDS.convert(now.getEpochSecond(), SECONDS) + now.getNano();
var payload = MessageFormat.format(template, "demo", String.valueOf(nowInEpochNanos), "Hello from Java App"); //1
var request = HttpRequest.newBuilder() //2
.uri(new URI("http://localhost:3100/loki/api/v1/push"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); //3
}
- 这就是我们在过去进行字符串插值的方式
- 创建请求
- 发送它
原型在Grafana中运行良好,但代码存在诸多限制:
标签硬编码,仅支持发送单标签
- 所有配置均为硬编码,如URL不可配置
- 每条日志均单独发送请求,无缓冲机制,效率极低
- HTTP客户端同步操作,阻塞线程等待Loki响应
- 完全缺乏错误处理机制
- 不支持Loki提供的gzip压缩和Protobuf格式
-
最终,它与我们使用日志的方式完全无关,例如:
Javavar logger = // 获取日志记录器 logger.info("包含参数的消息 {}, {}", foo, bar);
强化版常规日志记录
要实现上述功能,我们需要选择一个日志记录实现。由于我对SLF4J和Logback较为熟悉,我将采用它们。别担心,同样的方法也适用于Log4J2。
我们需添加相关依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <!--1-->
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <!--2-->
<version>1.4.8</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.github.loki4j</groupId>
<artifactId>loki-logback-appender</artifactId> <!--3-->
<version>1.4.0</version>
<scope>runtime</scope>
</dependency>
- SLF4J作为接口
- Logback作为具体实现
- Logback专为SLF4J设计的附加器
现在,我们添加一个特定的Loki附加器:
<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender"> <!--1-->
<http>
<url>http://localhost:3100/loki/api/v1/push</url> <!--2-->
</http>
<format>
<label>
<pattern>app=demo,host=${HOSTNAME},level=%level</pattern> <!--3-->
</label>
<message>
<pattern>l=%level h=${HOSTNAME} c=%logger{20} t=%thread | %msg %ex</pattern> <!--4-->
</message>
<sortByTime>true</sortByTime>
</format>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
- loki附加器
- Loki URL
- 可添加任意数量的标签
- 常规Logback模式
我们的程序因此变得更加简洁明了:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana展示如下内容:
Docker日志记录
I’m running most of my demos on Docker Compose, so I’ll mention the Docker logging trick. When a container writes on the standard out, Docker saves it to a local file. The docker logs
command can access the file content.
然而,除了保存到本地文件之外,还有其他选项,例如,syslog
、Google Cloud、Splunk等。要选择不同的选项,需设置日志驱动程序。可以在整体Docker级别或单个容器级别配置驱动程序。
Loki提供其自身的插件。安装方法如下:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
至此,我们可以在容器应用中使用它:
services:
app:
build: .
logging:
driver: loki #1
options:
loki-url: http://localhost:3100/loki/api/v1/push #2
loki-external-labels: container_name={{.Name}},app=demo #3
- Loki日志驱动程序
- 推送目标URL
- 附加标签
结果如下所示。注意默认标签。
结论
从宏观角度看,Loki并无特别之处:它只是一个带有RESTful API的普通存储引擎。
有多种方法可以使用API。除了简单的直接使用外,我们已了解Java日志框架附加器和Docker的使用方式。其他方法包括通过Kubernetes边车Promtail抓取日志文件,或者在应用与Loki之间加入OpenTelemetry Collector以进行转换。
选择几乎是无限的。务必谨慎选择最适合您上下文的方案。
深入了解: