Een van mijn huidige presentaties richt zich op Observability in het algemeen en Distributed Tracing in het bijzonder, met een OpenTelemetry implementatie. Tijdens de demo laat ik zien hoe je de sporen van een eenvoudig gedistribueerd systeem kunt zien, bestaande uit de Apache APISIX API Gateway, een Kotlin-app met Spring Boot, een Python-app met Flask en een Rust-app met Axum.
Eerder dit jaar sprak ik en was ik aanwezig bij de Observability-ruimte op FOSDEM. Een van de presentaties demonstreerde de Grafana-stack: Mimir voor metrische gegevens, Tempo voor sporen en Loki voor logs. Ik was verrast hoe je van de ene naar de andere kon gaan. Daarom wilde ik hetzelfde bereiken in mijn demo, maar via OpenTelemetry om geen afhankelijkheid van de Grafana-stack te creëren.
In deze blogpost wil ik focussen op logs en Loki.
Loki Basisprincipes en Ons Eerste Programma
In het kern is Loki een logopslagengine:
Loki is een horizontaal schaalbaar, zeer beschikbaar, multi-tenant logaggregatiesysteem geïnspireerd door Prometheus. Het is ontworpen om zeer kosteneffectief en gemakkelijk te bedienen. Het indexeert niet de inhoud van de logs, maar eerder een reeks labels voor elke logstream.
Loki biedt een RESTful API om logs op te slaan en te lezen. Laten we een log pushen vanuit een Java-app. Loki verwacht de volgende payloadstructuur:
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
}
- Zo deden we String interpolatie in de oude tijd
- De aanvraag maken
- Verstuur het
Het prototype werkt, zoals te zien is in Grafana:
Echter, bevat de code veel beperkingen:
- Het label is hard-gecodeerd. Je kunt en moet een enkel label verzenden
- Alles is hard-gecodeerd; niets is configureerbaar, bijvoorbeeld de URL
- De code stuurt één verzoek voor elke log; het is enorm inefficiënt omdat er geen bufferen is
- De HTTP-client is synchroon, waardoor de thread geblokkeerd wordt terwijl deze wacht op Loki
- Er is helemaal geen foutafhandeling
- Loki biedt zowel gzip-compressie als Protobuf; geen van beide worden ondersteund door mijn code
-
Tenslotte is het volledig ongerelateerd aan de manier waarop we logs gebruiken,bijv.:
Javavar logger = // Haal logger op logger.info("Mijn bericht met parameters {}, {}", foo, bar);
Gewone logregistratie op volle toeren
Om de bovenstaande uitspraak te gebruiken, moeten we een logregistratie-implementatie kiezen. Omdat ik meer bekend ben met het, zal ik SLF4J en Logback gebruiken. Maak je geen zorgen; dezelfde aanpak werkt ook voor Log4J2.
We moeten de relevante afhankelijkheden toevoegen:
<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 is de interface
- Logback is de implementatie
- Logback appender toegewijd aan SLF4J
Nu voegen we een specifieke Loki appender toe:
<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>
- De loki appender
- Loki URL
- Zoveel labels als gewenst
- Reguliere Logback patroon
Ons programma is veel eenvoudiger geworden:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana toont het volgende:
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.
Er zijn echter andere opties dan opslaan in een lokaal bestand beschikbaar, bijv., syslog
, Google Cloud, Splunk, enz. Om een andere optie te kiezen, stelt men een log driver in. Deze kan men configureren op het algemene Docker niveau of per container.
Loki biedt zijn eigen plugin. Om deze te installeren:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
Op dit punt kunnen we het gebruiken voor onze container app:
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 driver
- URL om naartoe te pushen
- Aanvullende labels
Het resultaat is het volgende. Let op de standaard labels.
Conclusie
Vanuit een vogelvluchtperspectief is Loki niets bijzonders: het is een eenvoudige opslagmotor met een RESTful API erbovenop.
Er zijn verschillende benaderingen beschikbaar om de API te gebruiken. Naast de naïeve, hebben we een Java logging framework appender en Docker gezien. Andere benaderingen omvatten het schrapen van de logbestanden, bijv., Promtail, via een Kubernetes sidecar. Je zou ook een OpenTelemetry Collector tussen je app en Loki kunnen toevoegen om transformaties uit te voeren.
Opties zijn vrijwel onbeperkt. Wees voorzichtig om de best passende te kiezen voor je context.
Om verder te gaan: