Una delle mie attuali conferenze si concentra sull’osservabilità in generale e sulla Tracciatura Distribuita in particolare, con un’implementazione OpenTelemetry. Nella demo, mostro come è possibile visualizzare i tracciati di un semplice sistema distribuito composto dall’API Gateway Apache APISIX, un’app Kotlin con Spring Boot, un’app Python con Flask e un’app Rust con Axum.
All’inizio di quest’anno, ho parlato e partecipato alla sala di Osservabilità al FOSDEM. Una delle dimostrazioni ha mostrato lo stack Grafana: Mimir per le metriche, Tempo per i tracciati e Loki per i log. Sono stato piacevolmente sorpreso da come si potesse passare da uno all’altro. Pertanto, volevo ottenere lo stesso risultato nella mia demo ma attraverso OpenTelemetry per evitare di legarmi allo stack Grafana.
In questo post sul blog, voglio concentrarmi sui log e su Loki.
Nozioni di base su Loki e Il nostro Primo Programma
In sostanza, Loki è un motore di archiviazione dei log:
Loki è un sistema di aggregazione dei log scalabile orizzontalmente, altamente disponibile e multi-tenant ispirato da Prometheus. È progettato per essere molto economico e facile da gestire. Non indice il contenuto dei log, ma piuttosto un set di etichette per ogni flusso di log.
Loki fornisce un RESTful API per memorizzare e leggere i log. Inseriamo un log da un’app Java. Loki si aspetta la seguente struttura del payload:
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
}
- Ecco come facevamo l’interpolazione di stringhe ai vecchi tempi
- Creare la richiesta
- Inviare
Il prototipo funziona, come si vede in Grafana:
Tuttavia, il codice presenta molti limiti:
- L’etichetta è codificata in modo rigido. È possibile e necessario inviare un’etichetta singola
- Tutto è codificato in modo rigido; nulla è configurabile, ad esempio, l’URL
- Il codice invia una richiesta per ogni log; è estremamente inefficiente poiché non c’è buffering
- Il client HTTP è sincrono, quindi blocca il thread mentre aspetta Loki
- Nessuna gestione degli errori
- Loki offre sia la compressione gzip che Protobuf; nessuno dei due è supportato dal mio codice
-
Infine, è completamente irrilevante rispetto a come utilizziamo i log, ad esempio:
Javavar logger = // Ottieni logger logger.info("Il mio messaggio con parametri {}, {}", foo, bar);
Registrazione regolare su steroidi
Per utilizzare l’affermazione precedente, dobbiamo scegliere un’implementazione di registrazione. Essendo più familiare con essa, userò SLF4J e Logback. Non preoccuparti; lo stesso approccio funziona per Log4J2.
Dobbiamo aggiungere le dipendenze pertinenti:
<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 è l’interfaccia
- Logback è l’implementazione
- Appender Logback dedicato a SLF4J
Ora, aggiungiamo un appender Loki specifico:
<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>
- L’appender Loki
- URL di Loki
- Tante etichette quante si vogliono
- Pattern Logback regolare
Il nostro programma è diventato molto più semplice:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana visualizza quanto segue:
Registrazione 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.
Tuttavia, sono disponibili altre opzioni oltre al salvataggio in un file locale, ad esempio, syslog
, Google Cloud, Splunk, ecc. Per scegliere un’opzione diversa, si imposta un driver di registrazione. È possibile configurare il driver a livello globale di Docker o per container.
Loki offre il proprio plugin. Per installarlo:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
A questo punto, possiamo usarlo sul nostro app container:
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
- Driver di registrazione Loki
- URL a cui inviare
- Etichette aggiuntive
Il risultato è il seguente. Nota le etichette predefinite.
Conclusione
Da un punto di vista d’insieme, Loki non è niente di straordinario: è un semplice motore di archiviazione con un’API RESTful in cima.
Sono disponibili diversi approcci per utilizzare l’API. Oltre al semplice, abbiamo visto un appender di framework di registrazione Java e Docker. Altri approcci includono lo scraping dei file di log, ad esempio, Promtail, tramite un sidecar Kubernetes. Potresti anche aggiungere un OpenTelemetry Collector tra la tua app e Loki per eseguire trasformazioni.
Le opzioni sono praticamente illimitate. Fai attenzione a scegliere quella che si adatta meglio al tuo contesto.
Per andare oltre: