TL/DR
En 2 briques, Logstash
? ETL ! Rust
? Vector !
On peut effectivement décrire Vector comme un Logstash qu’on aurait réécrit en Rust, avec tous les avantages inhérents au langage (performance, binaire natif), mais les défauts d’une solution encore très jeune (nombre de modules limités, peu de références).
On notera que la documentation semble plutôt bien faite, et que les composants natifs permettent de gérer beaucoup de use-case simple de monitoring.
Et comme on peut convenir que Logstash n’est pas le meilleur produit du monde, espérons que Vector trouve sa place dans les prochains mois dans la communauté.
Et en plus, ca tombe bien, Vector propose nativement des modules équivalents à ceux de Logstash, ce qui fait que la migration ne sera pas compliquée !
Après-tout, on a le temps non ? #Confinement
Vector se défini comme un routeur de données d’observabilité haute-performance, qui permet la collecte, la transformation et l’envoi d’événements (logs et/ou métriques).
Fonctionnement conceptuel
Il s’agit basiquement d’un ETL qui s’appuie sur les notions suivantes:
- Source (aka. E / Extract)
Récupération des données brutes depuis l’endroit où elles sont produites. Par exemple, on pourra lire les logs dans un fichier, écouter une file Kafka ou récupérer des métriques de StatsD
- Transform (aka. T / Transform)
Transformation des données brutes, ou du flux complet de données, Par exemple, on pourra filtrer des entrées ou encore parser un log selon une expression régulière
- Sink (aka. L / Load)
Destination des données lues et transformées. Chaque module enverra les données de manière unitaire ou sous la forme d’un stream en fonction du service cible. Par exemple, on pourra sauvegarder les données brutes sous Amazon S3, les indexer dans un cluster Elasticsearch ou les exposer à Prometheus
Fonctionnalités
Rapide
Ecrit en Rust, Vector est très rapide avec une gestion efficiente de la mémoire. Le tout sans runtime, ni garbage collector.
Un outil unique, de la source à la destination
Vector propose plusieurs stratégies de déploiement afin de pouvoir être utilisé par tous, quelque-soit le contexte.
Ici, on lancera une instance de Vector en tâche de fond pour collecter toutes les données du serveur hôte
Dans cette stratégie, on lancera une instance de Vector par service à monitorer.
Ici, Vector est lancé en tant qu’un service dédié.
En utilisant et/ou combinant ces stratégies, on peut donc définir des topologies d’architecture de collecte de données
Dans cette topologie, chaque instance de Vector va directement envoyer les données au(x) service(s) cibles. Il s’agit du cas le plus simple, et qui permet de scaler facilement. Néanmoins, il peut induire des pertes de performance locales ou de données.
Ici, chaque instance de Vector va envoyer les données à une instance centrale, chargée d’effectuer les opérations les plus coûteuses.
De fait, moins d’impact sur les performances locales des applicatifs, mais un service centrale en tant que SPOF
Variante de la topologie précédente, dans laquelle on va rajouter un broker en amont du service centrale afin de supprimer le SPOF.
Cette topologie est la plus scalable et durable, mais aussi la plus complexe à mettre en place.
Simplicité de déploiement
Concu en Rust, Vector se présente donc sous la forme d’un binaire cross-compilé pour l’os cible, et ne nécessite pas de runtime type JVM
Bon, OK, mais ca marche au moins ?
Pour tester Vector, je vais m’inspirer d’un post précédent : Une stack ELK from scratch avec Docker
Architecture de notre POC
Dans le cas présent, je vais m’appuyer sur :
- Elasticsearch, comme moteur d’indexation, de recherche & d’analytics,
- Kibana, comme IHM de visualisation et de génération de tableaux de bord interactifs
- Vector, en tant que service central, pour transformer les données et les envoyer vers Elasticsearch,
- Kafka, en tant que broker en amont de ma stack de monitoring
- Vector, en tant qu’agent, pour récupérer les données sources et les envoyer vers Kafka
On se positionne donc dans la topologie de Collecte Streamée décrite ci-avant
L’ensemble des services et des interactions sont décrites dans un fichier docker-compose.yml:
version: "3.7" | |
services: | |
zookeeper: | |
image: confluentinc/cp-zookeeper:5.4.0 | |
hostname: zookeeper | |
container_name: zookeeper | |
ports: | |
- "2181:2181" | |
environment: | |
ZOOKEEPER_CLIENT_PORT: 2181 | |
ZOOKEEPER_TICK_TIME: 2000 | |
kafka: | |
image: confluentinc/cp-enterprise-kafka:5.4.0 | |
hostname: kafka | |
container_name: kafka | |
depends_on: | |
- zookeeper | |
ports: | |
- "29092:29092" | |
- "9092:9092" | |
environment: | |
KAFKA_BROKER_ID: 1 | |
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' | |
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT | |
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 | |
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 | |
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 | |
elasticsearch: | |
image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2 | |
container_name: elastic | |
environment: | |
- ES_JAVA_OPTS=-Xms1g -Xmx1g | |
- discovery.type=single-node | |
- network.host=_site_, _local_ | |
ulimits: | |
memlock: | |
soft: -1 | |
hard: -1 | |
ports: | |
- 9200:9200 | |
- 9300:9300 | |
vector: | |
image: timberio/vector:0.8.0-alpine | |
container_name: vector | |
ports: | |
- 8888:8888 | |
volumes: | |
- $PWD/vector.toml:/etc/vector/vector.toml:ro | |
depends_on: | |
- kafka | |
- elasticsearch | |
kibana: | |
image: docker.elastic.co/kibana/kibana:7.6.2 | |
container_name: kibana | |
ports: | |
- 5601:5601 | |
depends_on: | |
- elasticsearch | |
webapp: | |
build: ./webapp/ | |
container_name: webapp | |
ports: | |
- 80:80 | |
- 9999:9999 |
Le service central Vector est configuré comme suit:
- Lecture des événements depuis le broker Kafka
- Parsing du JSON de l’événement envoyé depuis l’agent Vector
- Parsing Grok (format Logstash) de la ligne de log brute
- Indexation vers Elasticsearch
# Set global options | |
data_dir = "/var/lib/vector" | |
[sources.from_broker] | |
type = "kafka" | |
bootstrap_servers = "kafka:29092" | |
group_id = "vector-consumer" | |
topics = ["events"] | |
[transforms.json_parser] | |
type = "json_parser" | |
inputs = ["from_broker"] | |
drop_field = true | |
field = "message" | |
[transforms.log_parser] | |
type = "grok_parser" | |
inputs = ["json_parser"] | |
pattern = '%{IPORHOST:client} - %{USERNAME:user} \[%{HTTPDATE:timestamp}\] \"%{WORD:verb} %{NOTSPACE:path} HTTP/%{NUMBER}\" %{INT:status} %{NUMBER:bytes} \"%{DATA:referer}\" \"%{DATA:user_agent}\"' | |
types.status = "int" | |
types.bytes = "int" | |
types.timestamp = "timestamp|%d/%b/%Y:%H:%M:%S %z" | |
[sinks.to_indexer] | |
type = "elasticsearch" | |
inputs = ["log_parser"] | |
healthcheck = false | |
host = "http://elasticsearch:9200" | |
[[tests]] | |
name = "test_log_parser" | |
[[tests.inputs]] | |
insert_at = "json_parser" | |
type = "raw" | |
value = '172.21.0.1 - - [28/Feb/2020:12:38:46 +0000] "GET /path/to/a HTTP/1.1" 200 46459 "http://localhost/path/to/b" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36" "-"' | |
[[tests.outputs]] | |
extract_from = "log_parser" | |
[[tests.outputs.conditions]] | |
type = "check_fields" | |
"client.equals" = "172.21.0.1" | |
"user.equals" = "-" | |
"timestamp.equals"= "2020-02-28T12:38:46Z" | |
"verb.equals" = "GET" | |
"path.equals" = "/path/to/a" | |
"status.equals" = 200 | |
"bytes.equals" = 46459 | |
"referer.equals" = "http://localhost/path/to/b" | |
"user_agent.equals" = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36" |
Fun fact, il est possible de tester unitairement la configuration avec Vector, comme on peut le voir dans la section [[tests]] du fichier
On pourra également noter que chaque step de configuration se base sur au moins un step précédent.
Côté webapp, on ajoute un agent Vector configuré comme suit:
- Lecture des logs depuis un fichier
- Envoi vers le broker Kafka
# Set global options | |
data_dir = "/var/lib/vector" | |
[sources.from_file] | |
type = "file" | |
include = ["/var/log/nginx/*.log"] | |
[sinks.to_broker] | |
type = "kafka" | |
inputs = ["from_file"] | |
bootstrap_servers = "kafka:29092" | |
topic = "events" | |
encoding = "json" |
Le projet de test complet est disponible sur github discovering_vector
Il ne me reste qu’à lancer tous mes services
docker-compose build
docker-compose up
Puis me rendre sur ma webapp (dans mon cas, http://localhost:80) Exemple d’application web (source: https://github.com/sbilly/joli-admin)
Après une rapide navigation, je peux me rendre sur mon IHM Kibana (dans mon cas, http://localhost:5601), puis dans l’onglet Management
puis Kibana > Index Patterns
Ajout du pattern d’index vector-*
Et voilà ! Un index vector-YYYY.MM.DD
a été crée et contient bien mes logs applicatifs.
A partir de là, je vais pouvoir créer mes recherches, visualisations, dashboards et autre canvas dans Kibana, et pouvoir utiliser ces informations de monitoring.
Pour conclure, il est effectivement assez facile d’utiliser Vector comme remplacant de logtash/beats dans une stack Elastic, et le fait est que ca fonctionne. Reste à voir sur la durée si les gains de performance annoncé sont réels, et si le projet arrive durablement à s’imposer dans la communauté. En attendant, même très jeune, ce projet est plein de promesses et de bonnes idées (tests unitaires, multi-topologies natives, …), et mérite donc qu’on y jette un oeil!