< Home

A la découverte de Vector

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
view raw docker-compose.yml hosted with ❤ by GitHub

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"
view raw vector.toml hosted with ❤ by GitHub

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"
view raw agent_vector.toml hosted with ❤ by GitHub

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!