Aller au contenu
Outils medium

Fluent Bit : Collecteur de logs ultra-léger

22 min de lecture

Fluent Bit est un collecteur de logs ultra-léger, écrit en C, projet CNCF Graduated. Il consomme quelques mégaoctets de mémoire et s'est imposé comme l'agent de collecte de logs de référence sur chaque nœud Kubernetes, avant d'envoyer vers un agrégateur ou un backend comme Loki ou Elasticsearch.

Ce guide s'adresse aux profils intermédiaires et avancés qui exploitent des logs à grande échelle. Vous allez installer Fluent Bit, comprendre son pipeline (input, parser, filter, output), le configurer pour Kubernetes, écrire des parsers et router les logs vers Loki.

  • Comprendre le pipeline Fluent Bit : input, parser, filter, output.
  • Installer Fluent Bit proprement (dépôt signé, image épinglée).
  • Collecter les logs de conteneurs et les enrichir avec les métadonnées Kubernetes.
  • Écrire des parsers pour structurer les logs bruts.
  • Router les logs vers Loki, Elasticsearch ou un agrégateur.

Fluent Bit est conçu pour les environnements contraints en ressources et les architectures cloud-native à grande échelle. Écrit en C, sans machine virtuelle ni interpréteur à charger au démarrage, il tourne sur chaque nœud d'un cluster sans amputer le budget mémoire des applications qui y sont déjà déployées. Le tableau ci-dessous rassemble les chiffres qui décident de son adoption : une empreinte de l'ordre du mégaoctet et une licence Apache 2.0 qui n'impose aucune contrepartie commerciale.

CaractéristiqueDescription
LicenceApache 2.0
MaturitéCNCF Graduated
LangageC
Mémoire~1 MB
Throughput100k+ events/sec

Les deux outils viennent du même projet mais ne jouent pas le même rôle. Fluent Bit se place en collecteur d'edge sur chaque nœud, là où la ressource est comptée ; Fluentd sert d'agrégateur central qui reçoit les flux, applique des transformations lourdes et s'appuie sur un écosystème de plugins bien plus vaste, au prix d'un runtime Ruby à héberger. Les deux se chaînent parfaitement : Fluent Bit collecte et préfiltre, Fluentd agrège et route.

CritèreFluent BitFluentd
Mémoire~1 MB~40 MB
LangageCRuby
Plugins100+ intégrés500+ externes
RôleCollecte (edge)Agrégation (central)
PerformancesPlus rapidePlus flexible

Un événement traverse toujours les mêmes étages avant de quitter Fluent Bit. L'input lit la source (fichier, journal systemd, socket réseau), le parser découpe la ligne brute en champs exploitables, le filter enrichit ou écarte des enregistrements, l'output écrit vers la destination finale. Chaque événement porte un tag attribué par son input, et c'est sur ce tag que les directives Match des filtres et des sorties décident quoi traiter.

┌─────────────────────────────────────────────────────────────────┐
│ Fluent Bit Pipeline │
│ │
│ ┌────────┐ ┌────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ Input │──│ Parser │──│ Filter │──│ Output │ │
│ └────────┘ └────────┘ └─────────────┘ └──────────────────┘ │
│ │
│ - tail - JSON - kubernetes - loki │
│ - systemd - regex - record_modifier - elasticsearch │
│ - forward - logfmt - grep - forward │
│ - syslog - docker - lua - prometheus_exporter│
└─────────────────────────────────────────────────────────────────┘

Trois modes couvrent la quasi-totalité des besoins : le paquet système pour un serveur classique, l'image de conteneur pour un essai rapide, le chart Helm pour un cluster Kubernetes. La voie Debian passe par un dépôt signé dont la clé GPG est déposée dans /usr/share/keyrings, jamais par un script téléchargé et exécuté à la volée.

Fenêtre de terminal
# Clé GPG officielle Fluent Bit (dépôt signé, pas de pipe-to-shell)
sudo mkdir -p /usr/share/keyrings
curl -fsSL https://packages.fluentbit.io/fluentbit.key \
| sudo gpg --dearmor -o /usr/share/keyrings/fluentbit-keyring.gpg
# Dépôt signé (adapter le nom de version : noble, jammy, bookworm...)
echo "deb [signed-by=/usr/share/keyrings/fluentbit-keyring.gpg] https://packages.fluentbit.io/ubuntu/noble noble main" \
| sudo tee /etc/apt/sources.list.d/fluent-bit.list
sudo apt-get update && sudo apt-get install -y fluent-bit
sudo systemctl enable --now fluent-bit
fluent-bit --version

Fluent Bit supporte deux formats de configuration : classique et YAML. Le format classique (.conf) est historique et reste majoritaire dans les exemples trouvés en ligne ; le format YAML, arrivé plus tard, s'intègre mieux dans une ConfigMap Kubernetes. Les deux décrivent les mêmes objets : un bloc SERVICE global, puis des inputs, des filters et des outputs. Un même fichier ne mélange pas les deux syntaxes, il faut choisir.

Chaque section est annoncée entre crochets et ses clés sont indentées en dessous ; parsers_file charge le fichier de parsers auquel les inputs font ensuite référence par leur nom.

fluent-bit.conf
[SERVICE]
flush 1
log_level info
parsers_file parsers.conf
[INPUT]
name tail
path /var/log/containers/*.log
tag kube.*
parser docker
[FILTER]
name kubernetes
match kube.*
[OUTPUT]
name stdout
match *

Tout est regroupé sous une clé pipeline, ce qui permet de coller la configuration telle quelle dans un manifeste sans manipuler de bloc de texte multiligne.

fluent-bit.yaml
service:
flush: 1
log_level: info
pipeline:
inputs:
- name: tail
path: /var/log/containers/*.log
tag: kube.*
parser: cri
filters:
- name: kubernetes
match: kube.*
merge_log: on
k8s-logging.parser: on
outputs:
- name: loki
match: "*"
host: loki-gateway
port: 3100
labels: job=fluentbit

Sur un cluster, le kubelet écrit la sortie standard de chaque conteneur dans /var/log/containers/, sous forme de liens symboliques vers /var/log/pods/. Fluent Bit tourne donc en DaemonSet, avec ces répertoires montés en lecture seule, puis interroge l'API Kubernetes pour rattacher à chaque ligne le pod, le namespace et les labels qui lui correspondent. Sans cet enrichissement, un log reste une chaîne de caractères sans propriétaire identifiable une fois arrivée dans le backend.

Ce fichier de valeurs est celui du chart officiel : chaque clé porte un bloc de configuration brut qui sera injecté tel quel dans la ConfigMap montée par les pods.

values.yaml
config:
service: |
[SERVICE]
Daemon Off
Flush 1
Log_Level info
Parsers_File parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
Health_Check On
inputs: |
[INPUT]
Name tail
Path /var/log/containers/*.log
multiline.parser docker, cri
Tag kube.*
Mem_Buf_Limit 50MB
Skip_Long_Lines On
Refresh_Interval 10
filters: |
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
Keep_Log Off
K8S-Logging.Parser On
K8S-Logging.Exclude On
Labels On
Annotations Off
[FILTER]
Name nest
Match kube.*
Operation lift
Nested_under kubernetes
Add_prefix kubernetes_
outputs: |
[OUTPUT]
Name loki
Match kube.*
Host loki-gateway.logging.svc
Port 3100
Labels job=fluentbit, namespace=$kubernetes_namespace_name
Remove_keys kubernetes_container_hash,kubernetes_docker_id
customParsers: |
[PARSER]
Name json
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z

Le DaemonSet garantit un pod de collecte par nœud, y compris sur les nœuds ajoutés plus tard. La limits.memory déclarée ici doit rester cohérente avec le Mem_Buf_Limit des inputs : si le buffer autorisé dépasse la limite du conteneur, le kubelet tue le pod (OOMKilled) avant que Fluent Bit ait eu l'occasion de freiner sa lecture.

fluent-bit-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: logging
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
serviceAccountName: fluent-bit
containers:
- name: fluent-bit
image: fluent/fluent-bit:4.0.5@sha256:ef17f25f3a76c11a267f95467cda283dddd69bdd1079f5134b0926a7998ba356
ports:
- containerPort: 2020
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: config
mountPath: /fluent-bit/etc/
resources:
limits:
memory: 100Mi
cpu: 100m
requests:
memory: 50Mi
cpu: 50m
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: config
configMap:
name: fluent-bit-config

Les parsers transforment les logs bruts en structure JSON. Sans parser, la ligne reste un champ log unique et monolithique : le niveau de sévérité, le code HTTP ou la durée de la requête restent noyés dans le texte, donc impossibles à filtrer côté backend. Un parser se déclare une seule fois dans parsers.conf, puis se référence par son nom depuis un input ou depuis le filtre kubernetes. Le choix dépend du runtime de conteneurs : cri pour containerd et CRI-O, docker pour l'ancien pilote de journalisation JSON.

Ces quatre définitions couvrent les sources les plus fréquentes. Time_Key désigne le champ qui porte l'horodatage et Time_Format la manière de le décoder ; sans eux, Fluent Bit date l'événement à l'heure de sa lecture et non à celle de son émission.

parsers.conf
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
[PARSER]
Name cri
Format regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<log>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
[PARSER]
Name syslog-rfc3164
Format regex
Regex /^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$/
Time_Key time
Time_Format %b %d %H:%M:%S
[PARSER]
Name json_logs
Format json
Time_Key timestamp
Time_Format %Y-%m-%dT%H:%M:%S.%LZ

Pour un format maison, un parser de type regex avec des groupes nommés (?<champ>...) produit directement les clés du JSON de sortie : chaque nom de groupe devient un nom de champ.

[PARSER]
Name nginx_access
Format regex
Regex ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z

Les filtres s'appliquent entre le parsing et la sortie, dans l'ordre de déclaration du fichier de configuration. Ils servent à trois choses : enrichir un enregistrement avec des champs supplémentaires, écarter ce qui ne mérite pas d'être stocké, et remodeler la structure avant l'envoi. Comme les sorties, chaque filtre ne traite que les événements dont le tag correspond à sa directive Match.

Enrichit chaque log avec les métadonnées du pod. Le filtre interroge l'API server avec le token du ServiceAccount monté dans le pod, ce qui suppose un rôle RBAC autorisé à lire les pods du cluster ; sans cette autorisation, les champs d'enrichissement restent vides sans que la collecte s'arrête.

[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Merge_Log On
K8S-Logging.Parser On
K8S-Logging.Exclude On
Labels On
Annotations Off

Ce filtre ajoute des champs constants à tous les enregistrements sélectionnés. C'est ce qui permet de distinguer la provenance des logs une fois plusieurs clusters regroupés dans le même backend.

[FILTER]
Name record_modifier
Match *
Record cluster production-us-east-1
Record environment prod

La directive Regex ne conserve que les enregistrements dont le champ visé correspond au motif ; Exclude fait l'inverse et les rejette. C'est le levier le plus direct pour réduire le volume facturé par le backend.

# Exclure les logs de health checks
[FILTER]
Name grep
Match kube.*
Exclude log /health|/ready|/metrics/
# Garder seulement les erreurs
[FILTER]
Name grep
Match kube.*
Regex level (error|fatal|critical)

Quand aucun filtre intégré ne suffit, un script Lua reçoit chaque enregistrement et renvoie un code qui décide de son sort : 1 pour un enregistrement modifié, 0 pour un enregistrement laissé intact, -1 pour le supprimer du flux.

[FILTER]
Name lua
Match *
script /fluent-bit/scripts/transform.lua
call transform
# transform.lua
function transform(tag, timestamp, record)
if record["level"] == nil then
record["level"] = "info"
end
return 1, timestamp, record
end

Une sortie écrit les événements vers leur destination finale et gère seule les échecs : si le backend ne répond pas, le scheduler de Fluent Bit réessaie avec un délai croissant pendant que les données en attente s'accumulent dans le buffer. Plusieurs sorties cohabitent sans problème dans la même configuration ; il suffit que leurs directives Match sélectionnent les mêmes tags pour dupliquer un flux vers deux destinations.

L'option Auto_Kubernetes_Labels on promeut les labels du pod en labels Loki. À manier avec prudence : chaque combinaison de labels crée un flux distinct côté Loki, et une cardinalité trop élevée dégrade fortement les performances d'indexation.

[OUTPUT]
Name loki
Match *
Host loki-gateway
Port 3100
Labels job=fluentbit, app=$kubernetes_labels['app']
Line_Format json
Auto_Kubernetes_Labels on

Suppress_Type_Name On est indispensable à partir d'Elasticsearch 8, qui a supprimé la notion de type de document : sans cette option, le serveur rejette les requêtes d'indexation. Logstash_Format On fait générer un nom d'index daté, compatible avec les politiques de rétention par index.

[OUTPUT]
Name es
Match *
Host elasticsearch
Port 9200
Index logs-%Y.%m.%d
Type _doc
Suppress_Type_Name On
Logstash_Format On
Retry_Limit 5

Le protocole forward est le format d'échange natif de l'écosystème Fluentd. Require_ack_response True fait attendre l'accusé de réception de l'agrégateur avant de considérer un lot comme livré, ce qui évite de perdre silencieusement des logs quand la liaison se coupe.

[OUTPUT]
Name forward
Match *
Host fluentd-aggregator
Port 24224
Require_ack_response True

Deux sorties déclarées sur le même Match reçoivent chacune une copie du flux. Le cas typique associe un backend de recherche à chaud et un bucket objet pour l'archive longue durée, beaucoup moins coûteuse au gigaoctet.

# Dupliquer vers plusieurs destinations
[OUTPUT]
Name loki
Match *
Host loki
[OUTPUT]
Name s3
Match *
bucket my-logs-bucket
region eu-west-1
total_file_size 50M

Une trace d'exception Java ou Python s'étale sur plusieurs lignes, et chacune arrive comme un événement distinct : l'erreur se retrouve éparpillée en dix entrées illisibles dans le backend. Un multiline parser répare cela en définissant une règle de démarrage (start_state) et une règle de continuation (cont) pour recoller ces lignes en un seul enregistrement. Fluent Bit embarque déjà des règles prêtes à l'emploi (docker, cri, java, python, go) utilisables directement dans multiline.parser.

[MULTILINE_PARSER]
name java_multiline
type regex
flush_timeout 1000
rule "start_state" "^(\d{4}-\d{2}-\d{2})" "cont"
rule "cont" "^(?!\d{4}-\d{2}-\d{2})" "cont"
[INPUT]
name tail
path /var/log/app/*.log
multiline.parser java_multiline

Fluent Bit publie son propre état par une API HTTP interne, désactivée par défaut. Une fois activée, elle sert à la fois de sonde de vivacité pour Kubernetes et de source de métriques pour Prometheus : enregistrements lus par input, octets envoyés par output et, surtout, compteurs de tentatives et d'échecs qui trahissent un backend en difficulté bien avant que les utilisateurs ne signalent des logs manquants.

Les trois endpoints répondent sur le port 2020 déclaré ci-dessous : /api/v1/health pour les probes Kubernetes, les deux autres pour les métriques, respectivement au format JSON et au format Prometheus.

[SERVICE]
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
# Endpoints disponibles :
# GET /api/v1/health
# GET /api/v1/metrics
# GET /api/v1/metrics/prometheus

Cette ressource suppose l'opérateur Prometheus installé sur le cluster : c'est lui qui la lit pour générer la configuration de scraping. Chaque pod Fluent Bit est alors interrogé toutes les 30 secondes sur son endpoint Prometheus.

servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: fluent-bit
namespace: logging
spec:
selector:
matchLabels:
app: fluent-bit
endpoints:
- port: http
path: /api/v1/metrics/prometheus
interval: 30s

Ces quatre réflexes évitent les deux incidents les plus courants en production : le collecteur qui consomme plus de mémoire que l'application qu'il observe, et le flux de logs qui fait exploser la facture de stockage. Tous se configurent au moment de l'installation, avant que le volume ne devienne un problème à traiter dans l'urgence.

  1. Limitez la mémoire

    Configurez Mem_Buf_Limit pour éviter les OOM.

    [INPUT]
    Name tail
    Mem_Buf_Limit 50MB
  2. Activez les health checks

    [SERVICE]
    Health_Check On
  3. Utilisez des labels Kubernetes

    Filtrez par namespace pour ne pas tout envoyer.

    [FILTER]
    Name grep
    Match kube.*
    Regex kubernetes_namespace_name ^(prod-|staging-)
  4. Gérez le backpressure

    En cas de destination lente, évitez d'accumuler en mémoire.

Les pannes de collecte se ressemblent toutes : les logs n'arrivent plus et rien n'indique où la chaîne s'est rompue. Le tableau relie les symptômes observés à leur cause la plus probable ; les commandes qui suivent permettent de valider une configuration et de relire le pipeline en mode verbeux sans redémarrer le service de production.

SymptômeCause probableSolution
Logs manquantsPath incorrectVérifier le pattern glob
Parser échoueFormat non reconnuTester avec fluent-bit -c ... -i dummy
Mémoire élevéeBuffer pleinRéduire Mem_Buf_Limit
Connection refusedOutput downVérifier la destination
Fenêtre de terminal
# Test de configuration
fluent-bit -c /etc/fluent-bit/fluent-bit.conf --dry-run
# Mode debug
fluent-bit -c /etc/fluent-bit/fluent-bit.conf -vvv
# Parser test
echo '{"level":"info","msg":"test"}' | fluent-bit -i stdin -o stdout -p format=json

Les questions ci-dessous reviennent le plus souvent lors d'une première mise en production : positionnement face à Fluentd, choix du parser et envoi des entrées vers Loki.

  • Ultra-léger : ~1 MB de mémoire
  • CNCF Graduated : standard Kubernetes
  • Parser CRI/Docker : natif pour les conteneurs
  • Kubernetes filter : enrichissement auto des métadonnées
  • Multi-output : dupliquer vers plusieurs destinations

La documentation officielle répertorie l'intégralité des plugins avec toutes leurs options ; ce guide n'en couvre que les plus employés.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn