我目前的一场演讲专注于可观察性的一般概念以及分布式追踪的特定应用,特别是采用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的appender
現在,我們添加一個特定的Loki appender:
<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 appender
- Loki URL
- 想要多少標籤就添加多少
- 常規的Logback模式
我們的程式變得更加直接了:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana展示如下:
Docker Logging
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日誌框架appender和Docker。其他方法包括抓取日誌文件,例如,通過Kubernetes sidecar使用Promtail。您還可以在應用和Loki之間添加OpenTelemetry Collector來執行轉換。
選項幾乎是無限的。務必選擇最適合您情境的那一個。
進一步操作: