Centraliser et normaliser ses logs avec Vector
Lecture 16 min. âą
Table des matiĂšres

Bonjour! đ
Mon nombre de VMs a explosĂ© au cours de ces derniers mois. Je ne mâen plains pas, mais il devient de plus en plus fastidieux dâaller consulter mes logs.
Un autre de mes problÚmes est que mon infra est constituée de programmes écrits dans différents langages qui ont chacun leur maniÚre de rédiger leurs logs.
Une autre complication est lâutisation de Docker et Kubernetes qui enrobe les processus et rajoute une couche de complexitĂ© dans la comprĂ©hension du run des applications.
Jâavais tout dâabord commencĂ© Ă regarder le produit âSyslogâ qui a fait ces preuves. Mais celui-ci ne convenait pas Ă mes besoins car les messages gĂ©nĂ©rĂ©s contenanaient trop dâinformations parasites, ne permettant pas leur exploitation directe.
Câest alors que durant un de mes lives sur twitch lâun de mes spectateurs mâa indiquĂ© lâexistence dâun produit nommĂ© Vector.
Installation
Vector est un binaire qui peut-ĂȘtre installĂ© de diverses maniĂšres.
Je vais utiliser la mĂ©thode de lâinstallation par package.
curl -1sLf \
'https://repositories.timber.io/public/vector/cfg/setup/bash.deb.sh' \
| sudo -E bash
apt install vectorPrincipes de fonctionnement
Vector possĂšde une architecture fonctionnelle plutĂŽt directe.
Il peut prendre une ou plusieurs de sources de données, réaliser une suite de transformations sur ces évÚnements puis les distribuer vers une ou plusieurs sorties.
Lâensemble de ce processus est appelĂ© un pipeline.
Il est dĂ©crit dans un fichier de configuration utilisant le langage de description TOML. On peut le faire en YAML et en JSON mais ne parlons pas des sujets qui fĂąchent. đ
Sources
Les entrées du pipeline sont appelées des sources.
Vector vous proposent un vaste choix de sources qui vont des plus haut-niveau comme directement se connecter Ă lâAPI Docker pour rĂ©cupĂ©rer les logs des containers Ă des choses bien plus bas-niveau comme la lecture de socket ou de fichiers.
Une source est défini ainsi:
[source.ma_source]
type = "type de la source"Vous pouvez définir autant de sources que vous le désirez.
Lâidentifiant ma_source est le nom de votre entrĂ©e et permettra de lâidentifier au sein du pipeline.
Une source a pour sortie un Ă©vĂšnement sous la forme dâun message au format JSON. Chaque source possĂšde sa structure de sortie qui est documentĂ©e pour chacune dâelle.
Transformations
Chaque source peut bĂ©nĂ©ficier dâune ou plusieurs transformations. Les transformations sont des opĂ©rations qui sont rĂ©alisĂ©es sur les Ă©vĂšnements provenant des sources.
Une transformation peut prendre une ou plusieurs sources.
De mĂȘme que pour les sources, Vector propose lĂ aussi des transformations dĂ©jĂ existantes.
Une transformation se décrit sous cette forme.
[transforms.ma_transformation]
type = "type de la transformation"
inputs = ["ma_source_1", "ma_source_2"]Le champ inputs est la collection de flux dâentrĂ©es dĂ©finis par un ou plusieurs identifiants.
Il est aussi possible de chaĂźner les transformations pour rafiner les traitements.
[transforms.ma_transformation]
type = "type de la transformation"
inputs = ["ma_source_1", "ma_source_2"]
[transforms.ma_transformation_2]
type = "type de la transformation"
inputs = ["ma_transformation"]Remap
Dans les transformations, il en existe une qui est plus spĂ©ciale que les autres. Il sâagit de la transformation remap.
Celle ci a un format un peu différent qui prend un paramÚtre source.
Dans celui ci, il est possible de dĂ©finir des transformations personnalisĂ©es au moyen dâun langage de manipulation de donnĂ©es appelĂ© le VRL.
Ce langage est ensuite converti en du Rust, permettant de bénéficier de toutes les garanties concernant le typage des données manipulé ainsi que la faillibilité des appels de fonctions et de routines de traitement.
Le langage est complet et propose tous les outils nĂ©cessaire comme lâaffectatation de variable, la notion de condition, ainsi que de contextes et de blocs.
Sinks
Dans la terminologie de Vector les sorties de pipelines sont appelés des sinks.
De mĂȘme une sĂ©lection de sinks prĂ©-construits existe.
Un sink prend une collection de flux dâĂ©vĂ©nements et les dĂ©livre Ă sa destination.
Votre pipeline peut possĂ©der autant de sinks que vous dĂ©sirez et chaque sinks prend autant dâentrĂ©es que voulu.
[sinks.ma_sortie]
type = "type de ma sortie"
inputs = ["ma_source", "ma_transformation"]Cas pratique
Cahier des charges
Mon cas dâutilisation est le suivant. Je possĂšde une sĂ©rie de VMs qui tournent Ă lâintĂ©rieur dâun rĂ©seau.
Chacune de ces VMs possÚde une stack Docker décrite par un docker-compose.
Le but est de centraliser les logs de toutes ces VMs, en les regroupant par container et par jours.
Par exemple si nous avons 2 machines appelées respectivement:
- docker-001
- docker-002
Et possédant chacune le docker-compose suivant:
services:
tomcat:
image: tomcat
jetty:
image: jetty
js:
image: custom-jsAlors lâaborescence de logs finaux devra ressembler Ă :
logs
âââ by-host
âââ docker-001
â âââ docker
â âââ json
â â âââ tomcat
â â â âââ 13-09-2021.log
â â â âââ 14-09-2021.log
â â âââ jetty
â â â âââ 13-09-2021.log
â â â âââ 14-09-2021.log
â â âââ js
â â âââ 13-09-2021.log
â â âââ 14-09-2021.log
â âââ text
â âââ tomcat
â â âââ 13-09-2021.log
â â âââ 14-09-2021.log
â âââ jetty
â â âââ 13-09-2021.log
â â âââ 14-09-2021.log
â âââ js
â âââ 13-09-2021.log
â âââ 14-09-2021.log
âââ docker-002
âââ docker
âââ json
â âââ tomcat
â â âââ 13-09-2021.log
â â âââ 14-09-2021.log
â âââ jetty
â â âââ 13-09-2021.log
â â âââ 14-09-2021.log
â âââ js
â âââ 13-09-2021.log
â âââ 14-09-2021.log
âââ text
âââ tomcat
â âââ 13-09-2021.log
â âââ 14-09-2021.log
âââ jetty
â âââ 13-09-2021.log
â âââ 14-09-2021.log
âââ js
âââ 13-09-2021.log
âââ 14-09-2021.logNotre but va ĂȘtre de construire une topologie de centralisation et de normalisation de logs qui rĂ©ponde Ă ce besoin.
Nous utiliseront bien évidemment Vector pour la plupart des opérations.
Descriptif des contraintes
Tomcat et Jetty sont des serveurs HTTP Ă©crits tous les deux en Java mais ne gĂ©nĂšrent pas leur logs sous le mĂȘme format.
Une ligne de log Tomcat ressemblera Ă :
14-Sep-2021 06:48:31.489 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [102] millisecondsAlors que Jetty renverra plutĂŽt des messages de logs ainsi:
2021-09-14 06:48:34.560:INFO:oejs.AbstractConnector:main: Started ServerConnector@11245489{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}Nous avons aussi dans notre stack une application NodeJs qui possĂšde de mĂȘme, sa propre façon de crĂ©er ses logs.
Vous pouvez retrouvez dans ce projet les sources de lâimage NodeJS.
Cette image écrira ses logs sous cette forme:
2021-09-14T06:52:52.480Z [main] info: Application startedComme vous pouvez le voir chacun de ces logs signifie que lâapplication a dĂ©marrĂ© mais aucune de ces lignes nâest identique dâun container Ă un autre, ce qui complexifie grandement le traitement des logs de notre stack Docker.
Nous allons devoir normaliser tout ça! đ
Une autre contrainte est que les stacks Docker sont séparées les unes des autres dans des VMs pour des raisons de sécurité et scalabilité.
Il va donc falloir rappatrier ces logs normalisĂ©s au niveau dâun point unique qui se chargera de leur aggrĂ©gation.
Topologie
Une des rÚgles que nous allons essayer de nous astreindre est de ne pas générer inutilement de SPOF.
Ceci signifie que la stack Docker doit-ĂȘtre Ă mĂȘme de fonctionner lorsque le pipeline de logs est defectueux.
Donc avant dâĂȘtre poussĂ© vers lâaggrĂ©gateur, les logs normalisĂ©s seront conservĂ©s sur les VMs et rĂ©guliĂšrement effacĂ©s par un logrotate.
Récupérer les logs de ses containers
La premiĂšre difficultĂ© Ă laquelle nous allons ĂȘtre confrontĂ©, est de pouvoir rĂ©cupĂ©rer les logs qui sont gĂ©nĂ©rĂ©s par les applications encapsulĂ©es dans les containers.
Vector possĂšde une source Docker mais de lâaveu mĂȘme de la documentation, il est fortement dĂ©conseillĂ© de lâutiliser.
Nous allons donc devoir trouver une maniÚre détournée de récupérer ces logs.
Heureusement, Docker nous fourni une série de maniÚres de réaliser cette extraction.
Notre cahier des charges nous impose plusieurs contraintes. Il ne faut pas quâun dĂ©faut dans le systĂšme de pipeline de logs nâinterdise le fonctionnement de la stack Docker.
Cette difficultĂ© exclut dâemblĂ©e tous les systĂšmes dâenvoie de logs par protocole rĂ©seau. En effet si le pipeline dĂ©cĂšde ou est innacessible, les containers ne dĂ©marreront pas.
Les éliminés sont donc:
- syslog en réseau
- fluentd
- splunk en réseau
- graylog
- logentries

On peut aussi sortir les drivers spécifiques au GAFAM: AWS et GCP.
ETW étant pour windows, il ne nous aidera pas non plus sur le coup.
Il nous reste les stockages de logs locaux:
- local
- json-file
- journald
Local est trop simple et ne permet pas de gérer les métadata. Et le stockage JSON ne permet pas une recherche efficace en local.
Nous allons donc nous tourner vers un stockage de logs en utilisant journald.
Pour plus dâinformations:
- https://guillaume.fenollar.fr/blog/journald-tutoriel-journald-journalctl
- https://www.freedesktop.org/software/systemd/man/journalctl.html
Comme expliqué dans ces ressources, la grande force de journald est de permettre le stockage des méta-données des messages à des fins de filtrages.
Examinons tout dâabord ce que nous fourni le driver de logging journald de Docker.
En plus du message, nous avons accĂšs Ă plusieurs meta-data, dont:
CONTAINER_IDSYSLOG_IDENTIFIERouCONTAINER_TAGIMAGE_NAMEnon documenté mais présent dans mes expérimentations
Ceci constituera nos logs brutes.
En plus de ces mĂ©ta-data propres au driver docker. Il existe une sĂ©rie de mĂ©ta-datas constitutive du protocole journald dont la liste exhaustive est dĂ©finie dans lâexemple suivant.
Ceux qui vont particuliÚrement nous intéresser sont:
hosttimestamp__REALTIME_TIMESTAMP, le timestamp en microsecondes
Configurons nos container pour envoyer leur logs vers journald.
version: '3.7'
services:
tomcat:
image: tomcat
logging:
driver: journald
options:
tag: "java"
jetty:
image: jetty
logging:
driver: journald
options:
tag: "java"
js:
image: custom-js
logging:
driver: journald
options:
tag: "js"Nous rajoutons un tag qui nous permettra de traiter les messages plus facilement. Cette valeur sera définie dans la méta-donnée SYSLOG_IDENTIFIER.
Nous pouvons maintenant commencer la crĂ©ation de notre pipeline đ
Pour cela crée un fichier docker_logs_pipeline_normalization.toml
[sources.journald_docker_unit]
type = "journald"
include_units = ["docker"]
data_dir = "${PWD}/data"journald_docker_unit sera lâidentifiant de notre source. Puis on dĂ©finit cette source comme Ă©tant du type journald.
On filtre par seulement lâunit docker et on dĂ©fini un dossier qui contiendra le checkpoint de lecture du journal.
Ceci permet de ne pas retraiter plusieurs fois les mĂȘme logs.
Normaliser les évÚnements
Nous allons normaliser cet Ă©vĂšnement pour le rendre moins spĂ©cifique Ă docker. Ceci nous permettra dans lâavenir de plus facilement gĂ©rer dâautres sources.
Pour cela nous allons utiliser notre premiĂšre transformation.
[transforms.normalized_journald_docker_events]
inputs = ["journald_docker_unit"]
type = "remap"
source = '''
service_identifier = .IMAGE_NAME+"/"+.CONTAINER_ID ?? "unknown"
. = {
"message" : .message,
"timestamp": .timestamp,
"unix_timestamp" : .__REALTIME_TIMESTAMP,
"hostname" : .host,
"meta" : {
"type" : "docker",
"image" : .IMAGE_NAME,
"container_id" : .CONTAINER_ID,
"log_topology" : .SYSLOG_IDENTIFIER
}
}
.service_identifier = service_identifier
'''Cette transformation prend un évÚnement du type:
{
"CONTAINER_ID":"306bbf1dd696",
"CONTAINER_ID_FULL":"306bbf1dd69616a0435dec35fa2e3a7d73725915e7cd9f331f5da90dafd400f9",
"CONTAINER_NAME":"docker_tomcat_1",
"CONTAINER_TAG":"java",
"IMAGE_NAME":"tomcat",
"PRIORITY":"6",
"SYSLOG_IDENTIFIER":"java",
"_BOOT_ID":"79f4683a192e41218842e2c683972993",
"_CAP_EFFECTIVE":"3fffffffff",
"_CMDLINE":"/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock",
"_COMM":"dockerd",
"_EXE":"/usr/bin/dockerd",
"_GID":"0",
"_MACHINE_ID":"c9f961f08f12487d833a6198ff3d4ffe",
"_PID":"966",
"_SELINUX_CONTEXT":"unconfined\n",
"_SOURCE_REALTIME_TIMESTAMP":"163182514785904",
"_SYSTEMD_CGROUP":"/system.slice/docker.service",
"_SYSTEMD_INVOCATION_ID":"fc3ebfb5002d41eb9d0e364f60610d94",
"_SYSTEMD_SLICE":"system.slice",
"_SYSTEMD_UNIT":"docker.service",
"_TRANSPORT":"journal",
"_UID":"0",
"__MONOTONIC_TIMESTAMP":"254749478898",
"__REALTIME_TIMESTAMP":"1631825147859",
"host":"docker-001.example.com",
"message":"16-Sep-2021 20:45:47.859 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [146] milliseconds",
"source_type":"journald",
"timestamp":"2021-09-16T20:45:47.859335Z"
}Et le transforme en un document de ce type:
{
"hostname":"docker-001.example.com",
"message":"16-Sep-2021 20:45:47.859 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [146] milliseconds",
"meta":{
"container_id":"306bbf1dd696",
"image":"tomcat",
"log_topology":"java",
"type":"docker"
},
"service_identifier":"tomcat/306bbf1dd696",
"timestamp":"2021-09-16T20:45:47.859335Z",
"unix_timestamp":1631825147859
}De cette maniĂšre les logs peuvent venir de nâimporte quel service on ne conserve, les traces de docker que dans les mĂ©ta-donnĂ©es de lâĂ©vĂšnement.
Router les messages
En fonction du type dâapplication qui produit le log, nous devons effectuer des traitements diffĂ©rents.
Pour se faire, nous allons diviser le flux de messages en fonction quâil vienne dâune application Java ou NodeJs.
Nous allons utiliser une autre transformation: le routing.
[transforms.split_stream_by_log_topology]
type = "route"
inputs = ["normalized_journald_docker_events"]
[transforms.split_stream_by_log_topology.route]
js = '.meta.log_topology == "js"'
java = '.meta.log_topology == "java"'Cette transformation créé deux flux, un contenant tous les messages qui ont comme identifiant le tag java et un autre avec le tag js.
Extraire les données des messages de logs
Pour le moment nos messages de logs sont trop bruts pour pouvoir ĂȘtre exploitĂ©s par un systĂšme automatisĂ©.
Il faut extraire les informations de celui-ci.
Généralement les bibilothÚques de log dans les différents langages de programmation permettent de définir la sévérité des messages (INFO, WARN, ERROR, FATAL) ainsi que la partie de code qui a produit ce log.
Nous allons tenter dâextraire ces donnĂ©es.
Les logs Java
Nous avons deux types de containers; du Jetty et du Tomcat. Chacun dâeux Ă sa façon propre de gĂ©rer ses logs.
Une ligne de log Tomcat ressemblera Ă :
13-Sep-2021 05:47:49.513 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]Alors que Jetty générera plutÎt ceci:
2021-09-13 05:47:47.584:INFO:oejs.AbstractConnector:main: Started ServerConnector@41dddee8{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}Nous allons utiliser une autre transformation remap pour réaliser ce travail. Le VRL propose tout une série de parsers déjà intégrés.
Malheureusement, aucun dâeux nâest adaptĂ© aux logs tel que dĂ©finis par mes containers.
Il faut donc se rabattre sur nos bonne vieilles regexp. đ
Voici la transformation que je vous propose, il y aurait surement des axes dâamĂ©liorations possibles.
[transforms.parse_message_body_java]
inputs = ["split_stream_by_log_topology.java"]
type = "remap"
source = '''
structured = parse_regex(.message, r'^\d{2}-\w{3}-\d{4}\s(?:\d{2}:){2}\d{2}.\d{3}\s(?P<severity>\w+)\s\[[\w-]+\]\s(?P<path>[^\s]+)\s(?P<message>.*)$') ??
parse_regex(.message, r'^\d{4}-\d{2}-\d{2}\s(?:\d{2}:){2}\d{2}.\d{3}:(?P<severity>\w+):(?P<path>[\w\.-]+):(?P<section>[\w-]+):\s(?P<message>.*)$') ??
{ "message": .message }
.message_extracted = structured.message
.path = structured.path
.severity = "info"
if is_string(.severity) {
.severity = downcase(.severity)
}
'''Un mot sur la création de la variable structured. Elle est constituée de plusieurs parties.
Si on schématise cela donne:
structured = A ?? B ?? defaultA et B sont appelĂ©s des fonctions faillibles, cela signifie que lorsque quâelle Ă©choue une action doit ĂȘtre entreprise.
Il existe plusieurs moyen de gĂ©rer la faillibilitĂ© dâune fonction.
La premiĂšre est de la rendre infaillible en ajoutant un !.
structured = A!Dans ce cas si une erreur survient, le message est abandonnĂ© et ne continuera pas dans le pipeline. Câest la maniĂšre la plus rapide de gĂ©rer les erreurs mais la plus propre. câest lâĂ©quivalant de lever une exception dans le code.
Une deuxiĂšme solution est de traiter lâerreur Ă la maniĂšre de Golang en analysant lâerreur retournĂ©e.
structured, err = A
if err != null {
# on traite l'erreur
}Le VRL possĂšde son propre systĂšme de codes dâerreur.
La troisiĂšme et derniĂšre solution qui est Ă mon avis la plus propre, est la coalescence, il sâagit dâun opĂ©rateur qui permet de chaĂźner des fonctions faillibles.
structured = A ?? B ?? defaultIci, si A Ă©choue alors on Ă©xĂ©cute B et si B Ă©choue alors on prend la valeur de default. default doit absolument ĂȘtre infaillible sinon ça ne compilera pas.
Dans notre cas lâidĂ©e est dâessayer de parser le corps du message en supposant quâil sâagit dâun message Tomcat. Si la regex ne match pas, la fonction parse_regex Ă©choue et lâon passe au parse de message Jetty. Et si ça Ă©choue aussi on créé un document par dĂ©faut qui ne contient que le message.
Nous allons utiliser ce systĂšme de gestion dâerreurs pour gĂ©rer plusieurs types de formats de logs au sein dâune mĂȘme transformation.
13-Sep-2021 05:47:49.513 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
2021-09-13 05:47:47.584:INFO:oejs.AbstractConnector:main: Started ServerConnector@41dddee8{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}Vont respectivement donner ces valeurs Ă la variable structured:
{"message":"Initializing ProtocolHandler [\"http-nio-8080\"]","severity":"INFO","path":"org.apache.coyote.AbstractProtocol.init"}
{"message":"Started ServerConnector@41dddee8{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}","severity":"INFO","path":"oejs.AbstractConnector"}On en profite pour normaliser la sĂ©vĂ©ritĂ© du log. Mais on doit prendre quelques prĂ©cautions. En effet, toutes les lignes ne possĂ©deront pas de sĂ©vĂ©ritĂ©. Or la mĂ©thode VRL downcase prend exclusivement des chaĂźne de caractĂšres comme paramĂštre dâentrĂ©e.
Comme on peut le voir les données sont extraites et normalisées sous une forme commune qui facilite leur traitement ultérieur.
Finalement la sortie finale sera:
{
"hostname":"docker-001.example.com",
"message":"16-Sep-2021 20:45:47.859 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [146] milliseconds",
"message_extracted":"Server startup in [146] milliseconds",
"meta":{
"container_id":"306bbf1dd696",
"image":"tomcat",
"log_topology":"java",
"type":"docker"
},
"path":"org.apache.catalina.startup.Catalina.start",
"service_identifier":"tomcat/306bbf1dd696",
"severity":"info",
"timestamp":"2021-09-16T20:45:47.859335Z",
"unix_timestamp":1631825147859
}Les logs NodeJs
De la mĂȘme maniĂšre que pour les logs venant des containers java. Nous allons utiliser une transformation qui va venir extraire les donnĂ©es venant de nos lignes de logs.
[transforms.parse_message_body_js]
type = "remap"
inputs = ["split_stream_by_log_topology.js"]
source = '''
structured = parse_regex(.message, r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\s\[(?P<path>[\w_/-]+)\]\s(?P<severity>\w+):\s(?P<message>.*)$') ??
{ "message": .message }
.message_extracted = structured.message
.path = structured.path
.severity = "info"
if is_string(.severity) {
.severity = downcase(.severity)
}
'''Pour rappel un log NodeJs de notre application ressemble Ă :
2021-09-14T06:52:52.480Z [main] info: Application startedAprĂšs transformation:
{
"hostname":"docker-001.example.com",
"message":"2021-09-16T20:45:45.357Z [main] info: Application started",
"message_extracted":"Application started",
"meta":{
"container_id":"2ccdfff60bd2",
"image":"js",
"log_topology":"js",
"type":"docker"
},
"path":"main",
"service_identifier":"js/2ccdfff60bd2",
"severity":"info",
"timestamp":"2021-09-16T20:45:45.357827Z",
"unix_timestamp":1631825145357
}On a ici aussi fait en sorte que les messages aient des topologies identiques. Nous pourrons désormais traiter de maniÚre unique les logs venant du Java et du NodeJS.
Exporter nos logs normalisĂ©s vers lâaggrĂ©gateur
JusquâĂ prĂ©sent nous avons normalisĂ© nos logs mais il reste toujours sur notre machine, nous ne rĂ©solvons toujours pas le souci de la centralisation des logs.
Nous allons devoir les exporter vers une destination qui se chargera de leur aggrégation.
Et quoi de mieux que dâenvoyer vers un autre pipeline Vector! đ
Ce choix technique nous permettra de pouvoir réaliser tous les post-traitements voulus. Au contraire de les envoyer directement dans un store comme Elasticsearch ou autre.
Pour cela nous allons utiliser un sink Vector.
Sur la machine contenant la stack docker dont on veut extraire les logs,
on rajoute au pipeline:
[sinks.vector_aggregator]
type = "vector"
inputs = ["parse_message_body_java", "parse_message_body_js"]
address = "192.168.9.63:9000"
version = "1"Nous allons cette fois-ci prendre 2 flux en entrée, les messages java et NodeJs extraits et normalisés.
En suite on dĂ©finit lâaddresse de notre instance Vector qui sert dâaggrĂ©gateur.
Et câest tout đ
Vous pouvez dĂ©sormais envoyer dâune multitude de noeuds vos logs vers un point central.
DĂ©finition du pipeline dâaggrĂ©gation
Je vous ai dit que nous allions pousser nos logs vers un autre Vector, lui aussi va avoir besoin dâune configuration.
Créons un fichier aggregator.toml
Tout dâabord la source, une source Vector.
[sources.vector_server]
type = "vector"
address = "0.0.0.0:9000"
version = "1"On écoute sur le port 9000.
Ătant donnĂ© quâil sâagit juste de dĂ©montrer la faisabilitĂ© de la centralisation des logs. Nous allons nous contenter dâune simple sortie fichier. Mais il est bien Ă©videmment possible de faire des choses bien plus complexes, incluant des transformations, des aggrĂ©gations et des rĂ©ductions.
Déclarons donc un sink File.
[sinks.file_json_output]
inputs = ["vector_server"]
type = "file"
encoding = "ndjson"
path = "./logs/by-host/{{ hostname }}/{{ meta.type }}/json/{{ service_identifier }}/%Y-%m-%d.log"Nous allons dĂ©finir un autre sinks, qui nous permettra de lire les logs comme si nous Ă©tions dans le stdout/err de lâapplication.
[sinks.file_text_output]
inputs = ["vector_server"]
type = "file"
encoding = "text"
path = "./logs/by-host/{{ hostname }}/{{ meta.type }}/text/{{ service_identifier }}/%Y-%m-%d.log"Lift-off ! đ
On a tous nos pipelines! Il est temps de lancer tout ça! đ
Tout dâabord, installez Vector sur tout vos serveurs, aussi bien ceux qui contiendront les stack dont on veut rĂ©cupĂ©rer les logs que le noeud dâaggrĂ©gation.
Copiez y respectivement les fichiers docker_logs_pipeline_normalization.toml et aggregator.toml.
Lancez dans lâordre
vector --config aggregator.tomlPuis
vector --config docker_logs_pipeline_normalization.tomlSi vous faites lâinverse vous aurez une erreur car le pipeline dâextraction de logs essaie de se connecter au port 9000. Or celui nâest pas encore en Ă©coute.
Ceci me permet de vous prĂ©venir que si votre rĂ©seau nâest pas suffisamment stable, il se peut que des problĂšmes de connectivitĂ© apparaissent et des erreurs surviennent.
Ceci avait motivĂ© notre choix dĂšs le dĂ©but de confier la gestion des logs en premier lieu Ă journald puis Ă Vector. Si Vector tombe les logs ne seront pas perdu et la stack Docker continuera de tourner comme si de rien nâĂ©tait.
Dâune maniĂšre gĂ©nĂ©ral, un couplage faible entre systĂšmes est toujours a prĂ©fĂ©rĂ© lorsque que les contraintes le permettent. Ici par exemple nous avons perdu en rĂ©activitĂ© mais gagnĂ© en rĂ©sillience. Tout est une histoire de balance entre performance et sĂ©curitĂ©, câest un peu la dĂ©finition de lâingĂ©niĂ©rie. đ
Maintenant si vous lancez vos docker-compose up, vous devriez voir les fichiers de logs apparaĂźtre sur votre noeud dâaggrĂ©gation.
On résume
Il est temps de faire le bilan.
On possĂšde deux pipelines:
docker_logs_pipeline_normalization.toml
[sources.journald_docker_unit]
type = "journald"
include_units = ["docker"]
data_dir = "${PWD}/data"
[transforms.normalized_journald_docker_events]
inputs = ["journald_docker_unit"]
type = "remap"
source = '''
service_identifier = .IMAGE_NAME+"/"+.CONTAINER_ID ?? "unknown"
. = {
"message" : .message,
"timestamp": .timestamp,
"unix_timestamp" : .__REALTIME_TIMESTAMP,
"hostname" : .host,
"meta" : {
"type" : "docker",
"image" : .IMAGE_NAME,
"container_id" : .CONTAINER_ID,
"log_topology" : .SYSLOG_IDENTIFIER
}
}
.service_identifier = service_identifier
'''
[transforms.split_stream_by_log_topology]
type = "route"
inputs = ["normalized_journald_docker_events"]
[transforms.split_stream_by_log_topology.route]
js = '.meta.log_topology == "js"'
java = '.meta.log_topology == "java"'
[transforms.parse_message_body_java]
inputs = ["split_stream_by_log_topology.java"]
type = "remap"
source = '''
structured = parse_regex(.message, r'^\d{2}-\w{3}-\d{4}\s(?:\d{2}:){2}\d{2}.\d{3}\s(?P<severity>\w+)\s\[[\w-]+\]\s(?P<path>[^\s]+)\s(?P<message>.*)$') ??
parse_regex(.message, r'^\d{4}-\d{2}-\d{2}\s(?:\d{2}:){2}\d{2}.\d{3}:(?P<severity>\w+):(?P<path>[\w\.-]+):(?P<section>[\w-]+):\s(?P<message>.*)$') ??
{ "message": .message }
.message_extracted = structured.message
.path = structured.path
.severity = "info"
if is_string(.severity) {
.severity = downcase(.severity)
}
'''
[transforms.parse_message_body_js]
type = "remap"
inputs = ["split_stream_by_log_topology.js"]
source = '''
structured = parse_regex(.message, r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\s\[(?P<path>[\w_/-]+)\]\s(?P<severity>\w+):\s(?P<message>.*)$') ??
{ "message": .message }
.message_extracted = structured.message
.path = structured.path
.severity = "info"
if is_string(.severity) {
.severity = downcase(.severity)
}
'''
[sinks.vector_aggregator]
type = "vector"
inputs = ["parse_message_body_java", "parse_message_body_js"]
address = "192.168.9.63:9000"
version = "1"Et
aggregator.toml
[sources.vector_server]
type = "vector"
address = "0.0.0.0:9000"
version = "1"
[sinks.file_json_output]
inputs = ["vector_server"]
type = "file"
encoding = "ndjson"
path = "./logs/by-host/{{ hostname }}/{{ meta.type }}/json/{{ meta.type }}/{{ service_identifier }}/%Y-%m-%d.log"
[sinks.file_text_output]
inputs = ["vector_server"]
type = "file"
encoding = "text"
path = "./logs/by-host/{{ hostname }}/{{ meta.type }}/text/{{ service_identifier }}/%Y-%m-%d.log"LâintĂ©gralitĂ© des sources peut-ĂȘtre retrouvĂ© sur le projet.
Petits dessins parce que câest cool les dessins. đ

Voici ce qui se passe schématiquement sur un noeud.
Les applications crachent leurs logs sur journald.
Vector vient lire journald pour en extraire les lignes de logs.
Normalise et extrait les informations pertinentes puis retransmet par réseau à un autre Vector.
.
Les logs normalisés des différents noeuds transitent par réseau vers notre aggrégateur.
Nous pouvous tout Ă fait crĂ©er des docker-003, 004, ou 100. La topologie restera la mĂȘme.
Tant que votre rĂ©seau tient le traffic et que votre noeud dâaggrĂ©gation aussi il nây aura pas de souci.
Si votre trafic devient trop important, il faudra peut-ĂȘtre penser Ă une topologie diffĂ©rente qui fera apparaitre un broker de messages qui tiendra lieu de buffer pour gĂ©rer la back-pressure de votre traffic de logs.
Conclusion
Cet article est le premier dâune sĂ©rie sur lâObservabilitĂ©, une discipline de lâinformatique qui se base sur plusieurs concepts dont la gestion des logs.
Nous avons fait que la moitiĂ© du chemin concernant lâĂ©tude de nos logs, nous les avons normalisĂ©s et centralisĂ©s mais pas encore traitĂ©s et rendus intelligents.
Dans la suite des articles, nous tenteront de définir une logique de recherche de logs permettant la détection et la résolution de bugs.
Je nâai pas encore dĂ©cidĂ© de la technologie qui sera mis en place mais peut-ĂȘtre un couple Elasticsearch-Kibana ou des produits plus exotiques comme Loki.
JâespĂšre que cette petite prĂ©sentation des bases de la rĂ©alisation dâun systĂšme de pipeline de logs vous a plu et lâon se dit Ă plus tard pour la suite de nos aventures dans le monde merveilleux normalisĂ© et automatisĂ© de lâObservabilitĂ©! đ€©
Merci de mâavoir lu! đ