L’une de mes conférences actuelles porte sur l’Observabilité en général et le Suivi Distribué en particulier, avec une implémentation OpenTelemetry. Dans la démo, je montre comment vous pouvez voir les traces d’un système distribué simple composé du Gateway API Apache APISIX, une application Kotlin avec Spring Boot, une application Python avec Flask, et une application Rust avec Axum.
Au début de cette année, j’ai parlé et assisté à la salle Observabilité au FOSDEM. L’une des présentations a démontré la pile Grafana : Mimir pour les métriques, Tempo pour les traces et Loki pour les logs. J’ai été agréablement surpris de voir comment on pouvait passer de l’un à l’autre. Ainsi, je voulais réaliser la même chose dans ma démo mais via OpenTelemetry pour éviter de me lier à la pile Grafana.
Dans cet article de blog, je veux me concentrer sur les logs et Loki.
Les bases de Loki et Notre Premier Programme
Au cœur de Loki se trouve un moteur de stockage de logs:
Loki est un système d’agrégation de logs hautement scalable, hautement disponible et multi-locataire, inspiré par Prometheus. Il est conçu pour être très rentable et facile à opérer. Il n’indexe pas le contenu des logs, mais plutôt un ensemble de labels pour chaque flux de logs.
Loki fournit une API RESTful pour stocker et lire les logs. Poussons un log à partir d’une application Java. Loki attend la structure de charge utile suivante:
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
}
- Voici comment nous faisions de l’interpolation de chaînes dans le passé
- Créer la requête
- L’envoyer
Le prototype fonctionne, comme on peut le voir dans Grafana:
Cependant, le code présente de nombreuses limitations:
- L’étiquette est codée en dur. Vous pouvez et devez envoyer une étiquette unique
- Tout est codé en dur; rien n’est configurable, par exemple, l’URL
- Le code envoie une requête pour chaque journal; c’est très inefficace car il n’y a pas de tamponnage
- Le client HTTP est synchrone, bloquant donc le thread en attendant Loki
- Aucune gestion des erreurs
- Loki propose à la fois la compression gzip et Protobuf; aucun n’est pris en charge par mon code
-
Enfin, cela n’a rien à voir avec la manière dont nous utilisons les logs, par exemple:
Javavar logger = // Obtenir logger logger.info("Mon message avec paramètres {}, {}", foo, bar);
Journalisation ordinaire sur stéroïdes
Pour utiliser l’énoncé ci-dessus, nous devons choisir une implémentation de journalisation. Comme je suis plus familier avec, je vais utiliser SLF4J et Logback. Ne vous inquiétez pas; la même approche fonctionne pour Log4J2.
Nous devons ajouter les dépendances pertinentes:
<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 est l’interface
- Logback est l’implémentation
- Appendice Logback dédié à SLF4J
Maintenant, nous ajoutons un appendice Loki spécifique:
<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’appendice Loki
- URL de Loki
- Autant de labels que souhaité
- Modèle régulier Logback
Notre programme est devenu beaucoup plus simple:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana affiche ce qui suit:
Journalisation 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.
Cependant, d’autres options que le stockage dans un fichier local sont disponibles, par exemple, syslog
, Google Cloud, Splunk, etc. Pour choisir une option différente, on définit un pilote de journalisation. On peut configurer le pilote au niveau global de Docker ou par conteneur.
Loki propose son propre plugin. Pour l’installer:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
À ce stade, nous pouvons l’utiliser sur notre application conteneur:
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
- Pilote de journalisation Loki
- URL à laquelle envoyer
- Labels supplémentaires
Le résultat est le suivant. Notez les labels par défaut.
Conclusion
À la vue d’oiseau, Loki n’est rien d’extraordinaire : c’est un moteur de stockage simple avec une API RESTful en plus.
Plusieurs approches sont disponibles pour utiliser l’API. Au-delà de la simple, nous avons vu un appendice de framework de journalisation Java et Docker. D’autres approches incluent le ramassage des fichiers de log, par exemple, Promtail, via un sidecar Kubernetes. Vous pourriez également ajouter un Collecteur OpenTelemetry entre votre application et Loki pour effectuer des transformations.
Les options sont virtuellement illimitées. Soyez attentifs à choisir celle qui convient le mieux à votre contexte.
Pour aller plus loin: