Einer meiner aktuellen Vorträge konzentriert sich allgemein auf Observability und speziell auf Distributed Tracing, mit einer OpenTelemetry-Implementierung. Im Demo-Teil zeige ich, wie man die Spuren eines einfachen verteilten Systems sehen kann, bestehend aus dem Apache APISIX API Gateway, einer Kotlin-App mit Spring Boot, einer Python-App mit Flask und einer Rust-App mit Axum.
Anfang dieses Jahres sprach und besuchte ich den Observability-Raum auf FOSDEM. Ein Vortrag demonstrierte die Grafana-Stapel: Mimir für Metriken, Tempo für Spuren und Loki für Protokolle. Ich war erfreut, wie man von einem zum anderen wechseln konnte. Daher wollte ich das gleiche in meiner Demo erreichen, aber über OpenTelemetry, um eine Bindung an die Grafana-Stapel zu vermeiden.
In diesem Blog-Beitrag möchte ich mich auf Protokolle und Loki konzentrieren.
Loki Grundlagen und Unser Erstes Programm
Grundsätzlich ist Loki ein Protokoll-Speicher-Engine:
Loki ist ein horizontal skalierbares, hoch verfügbares, mehrfach genutztes System zur Protokollaggregation, inspiriert von Prometheus. Es ist so konzipiert, dass es sehr kostengünstig und einfach zu betreiben ist. Es indexiert nicht den Inhalt der Protokolle, sondern lediglich eine Reihe von Labels für jede Protokoll-Stream.
Loki bietet eine RESTful API zum Speichern und Lesen von Protokollen. Schieben wir ein Protokoll von einer Java-App. Loki erwartet die folgende Payload-Struktur:
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
}
- So haben wir String-Interpolation in alten Zeiten gemacht
- Erstellen Sie die Anfrage
- Senden Sie es
Das Prototyp funktioniert, wie in Grafana zu sehen:
Jedoch weist der Code viele Einschränkungen auf:
- Das Label ist hartcodiert. Sie können und müssen ein einzelnes Label senden
- Alles ist hartcodiert; nichts ist konfigurierbar, z.B. die URL
- Der Code sendet für jede Log-Nachricht eine Anfrage; es ist äußerst ineffizient, da es kein Puffern gibt
- Der HTTP-Client ist synchron und blockiert somit den Thread während der Wartezeit auf Loki
- Überhaupt keine Fehlerbehandlung
- Loki bietet sowohl gzip-Komprimierung als auch Protobuf; keiner von beiden wird von meinem Code unterstützt
-
Schließlich hat es absolut nichts mit der Art und Weise zu tun, wie wir Log-Nachrichten verwenden, z.B.:
Javavar logger = // Logger abrufen logger.info("Meine Nachricht mit Parametern {}, {}", foo, bar);
Regelmäßiges Logging auf höchstem Niveau
Um die obige Aussage zu nutzen, müssen wir eine Logging-Implementierung auswählen. Da ich mich besser damit auskennen, werde ich SLF4J und Logback verwenden. Keine Sorge, dieselbe Vorgehensweise funktioniert auch für Log4J2.
Wir müssen die entsprechenden Abhängigkeiten hinzufügen:
<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 ist die Schnittstelle
- Logback ist die Implementierung
- Logback-Appender speziell für SLF4J
Jetzt fügen wir einen spezifischen Loki-Appender hinzu:
<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>
- Der Loki-Appender
- Loki-URL
- So viele Labels wie gewünscht
- Reguläres Logback-Muster
Unser Programm ist nun viel einfacher geworden:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana zeigt folgendes an:
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.
Es stehen jedoch auch andere Optionen als das Speichern in einer lokalen Datei zur Verfügung, z.B., syslog
, Google Cloud, Splunk, etc. Um eine andere Option auszuwählen, setzt man einen Logging-Treiber. Man kann den Treiber auf der gesamten Docker-Ebene oder pro Container konfigurieren.
Loki bietet seinen eigenen Plugin. Zur Installation:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
An diesem Punkt können wir es in unserer Container-App verwenden:
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-Logging-Treiber
- URL zum Senden
- Zusätzliche Labels
Das Ergebnis sieht folgendermaßen aus. Beachte die Standard-Labels.
Schlussfolgerung
Aus der Vogelperspektive ist Loki nichts Außergewöhnliches: Es ist ein einfacher Speicher-Engine mit einer RESTful-API darüber.
Es stehen verschiedene Ansätze zur Verfügung, um die API zu nutzen. Neben dem naiven Ansatz haben wir einen Java-Logging-Framework-Appender und Docker gesehen. Andere Ansätze umfassen das Auslesen der Log-Dateien, z.B., Promtail, über einen Kubernetes-Sidecar. Sie könnten auch einen OpenTelemetry Collector zwischen Ihrer App und Loki hinzufügen, um Transformationen durchzuführen.
Die Optionen sind praktisch unbegrenzt. Achten Sie darauf, diejenige zu wählen, die Ihrem Kontext am besten entspricht.
Weiter geht’s: