Introduction
En déployant un nouveau systÚme, nous avons été confrontés à la nécessité de traiter un grand volume de journaux variés. Nous avons choisi ELK comme outil. Dans cet article, nous aborderons notre expérience dans la configuration de cette stack.
Nous ne visons pas Ă dĂ©crire toutes ses fonctionnalitĂ©s, mais souhaitons nous concentrer sur la rĂ©solution de problĂšmes pratiques. Cela est dĂ» au fait qu'avec un volume assez important de documentation et d'images prĂȘtes Ă l'emploi, il existe de nombreuses embĂ»ches, du moins pour nous, elles ont Ă©tĂ© dĂ©couvertes.
Nous avons dĂ©ployĂ© la stack via docker-compose. De plus, nous avions un docker-compose.yml bien Ă©crit qui nous a permis de mettre en place la stack pratiquement sans problĂšmes. Et nous avons eu l'impression que la victoire Ă©tait dĂ©jĂ proche, il nous suffisait d'ajuster un peu pour nos besoins et tout serait prĂȘt.
Malheureusement, la tentative de peaufiner le systÚme pour recevoir et traiter les journaux de notre application ne s'est pas avérée fructueuse. Ainsi, nous avons décidé qu'il valait la peine d'examiner chaque composant séparément, puis de revenir sur leurs interactions.
Nous avons donc commencé par logstash.
Environnement, déploiement, exécution de Logstash dans un conteneur
Pour le déploiement, nous utilisons docker-compose, les expériences décrites ici ont été menées sur MacOS et Ubuntu 18.0.4.
L'image de logstash que nous avons indiquée dans notre docker-compose.yml est docker.elastic.co/logstash/logstash:6.3.2
Nous allons l'utiliser pour nos expériences.
Pour lancer logstash, nous avons Ă©crit un docker-compose.yml distinct. Bien sĂ»r, nous aurions pu dĂ©marrer l'image depuis la ligne de commande, mais nous avions une tĂąche spĂ©cifique Ă rĂ©soudre, oĂč tout est lancĂ© depuis docker-compose.
BrÚve présentation des fichiers de configuration
Comme indiquĂ© dans la description, logstash peut ĂȘtre exĂ©cutĂ© pour un seul canal, dans ce cas, il faut lui fournir un fichier *.conf, ou pour plusieurs canaux, auquel cas il doit recevoir un fichier pipelines.yml, qui en retour fera rĂ©fĂ©rence aux fichiers .conf pour chaque canal.
Nous avons opté pour la seconde option. Cela nous a semblé plus universel et évolutif. Nous avons donc créé un pipelines.yml et établi un répertoire pipelines, dans lequel nous allons placer les fichiers .conf pour chaque canal.
Ă l'intĂ©rieur du conteneur, il y a un autre fichier de configuration â logstash.yml. Nous ne le touchons pas, nous l'utilisons tel quel.
Ainsi, la structure de nos répertoires est la suivante :

Pour la réception des données, nous considérons que c'est du tcp sur le port 5046, et pour la sortie, nous utiliserons stdout.
Voici une configuration simple pour le premier lancement. Puisque l'objectif initial est de lancer.
Donc, nous avons ce fichier docker-compose.yml
version: '3'
networks:
elk:
volumes:
elasticsearch:
driver: local
services:
logstash:
container_name: logstash_one_channel
image: docker.elastic.co/logstash/logstash:6.3.2
networks:
- elk
ports:
- 5046:5046
volumes:
- ./config/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
- ./config/pipelines:/usr/share/logstash/config/pipelines:ro
Que voyons-nous ici ?
- Les rĂ©seaux et volumes ont Ă©tĂ© pris du docker-compose.yml source (celui oĂč l'ensemble de la pile est lancĂ©e) et je pense qu'ils n'affectent pas beaucoup l'image globale.
- Nous créons un service (services) logstash à partir de l'image docker.elastic.co/logstash/logstash:6.3.2 et lui attribuons le nom logstash_one_channel.
- Nous redirigeons le port 5046 vers le mĂȘme port interne dans le conteneur.
- Nous mappons notre fichier de configuration des canaux ./config/pipelines.yml sur le fichier /usr/share/logstash/config/pipelines.yml Ă l'intĂ©rieur du conteneur, d'oĂč logstash va le prendre, et nous le rendons en lecture seule, juste au cas oĂč.
- Nous mappons le rĂ©pertoire ./config/pipelines, oĂč se trouvent nos fichiers de configuration des canaux, vers le rĂ©pertoire /usr/share/logstash/config/pipelines et le rendons Ă©galement en lecture seule.

Fichier pipelines.yml
- pipeline.id: HABR
pipeline.workers: 1
pipeline.batch.size: 1
path.config: "./config/pipelines/habr_pipeline.conf"
Ici est décrit un canal avec l'identifiant HABR et le chemin vers son fichier de configuration.
Et enfin le fichier « ./config/pipelines/habr_pipeline.conf"
input {
tcp {
port => "5046"
}
}
filter {
mutate {
add_field => [ "habra_field", "Hello Habr" ]
}
}
output {
stdout {
}
}
Ne nous attardons pas encore sur sa description, essayons de le lancer :
docker-compose up
Que voyons-nous ?
Le conteneur a démarré. Nous pouvons vérifier son fonctionnement :
echo '13123123123123123123123213123213' | nc localhost 5046
Et nous voyons dans la console du conteneur la réponse :

Mais en mĂȘme temps, nous voyons aussi :
logstash_one_channel | [2019-04-29T11:28:59,790][ERROR][logstash.licensechecker.licensereader] Impossible de rĂ©cupĂ©rer les informations de licence du serveur de licence {:message=>«Elasticsearch Unreachable: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch», âŠ
logstash_one_channel | [2019-04-29T11:28:59,894][INFO ][logstash.pipeline ] Pipeline démarré avec succÚs {:pipeline_id=>".monitoring-logstash", :thread=>"#"}
logstash_one_channel | [2019-04-29T11:28:59,988][INFO ][logstash.agent ] Pipelines en cours d'exécution {:count=>2, :running_pipelines=>[:HABR, :".monitoring-logstash"], :non_running_pipelines=>[]}
logstash_one_channel | [2019-04-29T11:29:00,015][ERROR][logstash.inputs.metrics ] X-Pack est installĂ© sur Logstash mais pas sur Elasticsearch. Veuillez installer X-Pack sur Elasticsearch pour utiliser la fonction de surveillance. D'autres fonctionnalitĂ©s peuvent ĂȘtre disponibles.
logstash_one_channel | [2019-04-29T11:29:00,526][INFO ][logstash.agent ] API Logstash démarré avec succÚs {:port=>9600}
logstash_one_channel | [2019-04-29T11:29:04,478][INFO ][logstash.outputs.elasticsearch] Exécution d'une vérification de santé pour voir si une connexion Elasticsearch fonctionne {:healthcheck_url=>http://elasticsearch:9200/, :path=>"/"}
logstash_one_channel | [2019-04-29T11:29:04,487][WARN ][logstash.outputs.elasticsearch] Tentative de rétablir la connexion à une instance ES morte, mais une erreur est survenue. {:url=>«:9200/», :error_type=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=>«Elasticsearch inaccessible : [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
logstash_one_channel | [2019-04-29T11:29:04,704][INFO ][logstash.licensechecker.licensereader] Exécution de la vérification de santé pour voir si la connexion à Elasticsearch fonctionne {:healthcheck_url=>http://elasticsearch:9200/, :path=>"/"}
logstash_one_channel | [2019-04-29T11:29:04,710][WARN ][logstash.licensechecker.licensereader] Tentative de rétablir la connexion à une instance ES morte, mais une erreur est survenue. {:url=>«:9200/», :error_type=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=>«Elasticsearch inaccessible : [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
Et notre log grimpe constamment.
Ici, j'ai mis en valeur en vert le message indiquant que le pipeline a bien Ă©tĂ© lancĂ©, en rouge â le message d'erreur, et en jaune â le message indiquant une tentative de connexion Ă :9200.
Cela se produit parce que dans logstash.conf, intĂ©grĂ© dans l'image, il y a une vĂ©rification de l'accessibilitĂ© d'elasticsearch. Logstash suppose qu'il fonctionne dans le cadre de la pile Elk, mais nous lâavons sĂ©parĂ©.
On peut travailler, mais ce n'est pas pratique.
La solution consiste à désactiver cette vérification via la variable d'environnement XPACK_MONITORING_ENABLED.
Nous apporterons une modification Ă docker-compose.yml et relancerons :
version: '3'
networks:
elk:
volumes:
elasticsearch:
driver: local
services:
logstash:
container_name: logstash_one_channel
image: docker.elastic.co/logstash/logstash:6.3.2
networks:
- elk
environment:
XPACK_MONITORING_ENABLED: "false"
ports:
- 5046:5046
volumes:
- .\/config\/pipelines.yml:\usr\/share\/logstash\/config\/pipelines.yml:ro
- .\/config\/pipelines:\usr\/share\/logstash\/config\/pipelines:ro
VoilĂ , maintenant tout va bien. Le conteneur est prĂȘt pour les expĂ©riences.
Nous pouvons à nouveau taper dans le terminal à cÎté :
echo '13123123123123123123123213123213' | nc localhost 5046
Et voir :
logstash_one_channel | {
logstash_one_channel | "message" => "13123123123123123123123213123213",
logstash_one_channel | "@timestamp" => 2019-04-29T11:43:44.582Z,
logstash_one_channel | "@version" => "1",
logstash_one_channel | "habra_field" => "Hello Habr",
logstash_one_channel | "host" => "gateway",
logstash_one_channel | "port" => 49418
logstash_one_channel | }
Travail dans un seul canal
Donc, nous avons dĂ©marrĂ©. Nous pouvons maintenant consacrer du temps Ă la configuration de logstash en lui-mĂȘme. Ne touchons pas encore le fichier pipelines.yml, voyons ce que nous pouvons obtenir en travaillant avec un seul canal.
Il convient de noter que le principe général de travail avec le fichier de configuration du canal est bien décrit dans le guide officiel, le voici
Si vous souhaitez lire en français, nous avons utilisĂ© cet article (mais la syntaxe des requĂȘtes y est ancienne, il faut en tenir compte).
Passons en revue la section Input. Nous avons dĂ©jĂ vu le travail sur tcp. Qu'est-ce qui peut encore ĂȘtre intĂ©ressant ici ?
Messages de test, en utilisant heartbeat
Il existe une possibilité intéressante de générer des messages de test automatiques.
Pour cela, il faut inclure le plugin heartbeat dans la section input.
input {
heartbeat {
message => "HeartBeat!"
}
}
Nous activons, commençons aÌ recevoir toutes les minutes
logstash_one_channel | {
logstash_one_channel | "@timestamp" => 2019-04-29T13:52:04.567Z,
logstash_one_channel | "habra_field" => "Hello Habr",
logstash_one_channel | "message" => "HeartBeat!",
logstash_one_channel | "@version" => "1",
logstash_one_channel | "host" => "a0667e5c57ec"
logstash_one_channel | }
Nous voulons recevoir plus souvent, il faut ajouter le paramĂštre interval.
Ainsi, nous allons recevoir un message toutes les 10 secondes.
input {
heartbeat {
message => "HeartBeat!"
interval => 10
}
}
Lecture des données à partir d'un fichier
Nous avons également décidé de tester le mode file. Si ça fonctionne bien avec le fichier, alors il se pourrait qu'aucun agent ne soit nécessaire, du moins pour une utilisation locale.
D'aprĂšs la description, le mode de fonctionnement devrait ĂȘtre similaire Ă tail -f, c'est-Ă -dire qu'il lit les nouvelles lignes ou, en option, lit tout le fichier.
Alors, ce que nous voulons obtenir :
- Nous voulons recevoir les lignes qui sont ajoutées dans un fichier de log.
- Nous voulons recevoir les donnĂ©es qui sont enregistrĂ©es dans plusieurs fichiers de log, tout en ayant la possibilitĂ© de distinguer d'oĂč proviennent les donnĂ©es.
- Nous voulons vérifier qu'en redémarrant logstash, il ne recevra pas ces données à nouveau.
- Nous voulons vĂ©rifier que si logstash est dĂ©sactivĂ© et que les donnĂ©es continuent d'ĂȘtre Ă©crites dans les fichiers, alors quand nous le lancerons Ă nouveau, nous recevrons ces donnĂ©es.
Pour rĂ©aliser l'expĂ©rience, nous allons ajouter une ligne dans docker-compose.yml, en ouvrant le rĂ©pertoire oĂč nous plaçons les fichiers.
version: '3'
networks:
elk:
volumes:
elasticsearch:
driver: local
services:
logstash:
container_name: logstash_one_channel
image: docker.elastic.co/logstash/logstash:6.3.2
networks:
- elk
environment:
XPACK_MONITORING_ENABLED: "false"
ports:
- 5046:5046
volumes:
- ./config/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
- ./config/pipelines:/usr/share/logstash/config/pipelines:ro
- ./logs:/usr/share/logstash/input
Et nous allons modifier la section input dans habr_pipeline.conf
input {
file {
path => "/usr/share/logstash/input/*.log"
}
}
Nous démarrons :
docker-compose up
Pour créer et écrire des fichiers log, nous allons utiliser la commande :
echo '1' >> logs/number1.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hello Habr",
logstash_one_channel | "@timestamp" => 2019-04-29T14:28:53.876Z,
logstash_one_channel | "@version" => "1",
logstash_one_channel | "message" => "1",
logstash_one_channel | "path" => "/usr/share/logstash/input/number1.log"
logstash_one_channel | }
Ah, ça fonctionne !
Nous voyons donc que le champ path a été ajouté automatiquement. Cela signifie qu'à l'avenir, nous pourrons filtrer les enregistrements par celui-ci.
Essayons encore :
echo '2' >> logs/number1.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hello Habr",
logstash_one_channel | "@timestamp" => 2019-04-29T14:28:59.906Z,
logstash_one_channel | "@version" => "1",
logstash_one_channel | "message" => "2",
logstash_one_channel | "path" => "/usr/share/logstash/input/number1.log"
logstash_one_channel | }
Et maintenant dans un autre fichier :
echo '1' >> logs/number2.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hello Habr",
logstash_one_channel | "@timestamp" => 2019-04-29T14:29:26.061Z,
logstash_one_channel | "@version" => "1",
logstash_one_channel | "message" => "1",
logstash_one_channel | "path" => "/usr/share/logstash/input/number2.log"
logstash_one_channel | }
Super ! Le fichier a été pris en compte, le path est correct, tout va bien.
ArrĂȘtons logstash et relançons-le. Attente. Silencio. C'est-Ă -dire que nous ne recevons pas ces enregistrements Ă nouveau.
Et maintenant, l'expérience la plus audacieuse.
Nous arrĂȘtons logstash et exĂ©cutons :
echo '3' >> logs/number2.log
echo '4' >> logs/number1.log
Nous relançons logstash et voyons :
logstash_one_channel | {
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hello Habr",
logstash_one_channel | "message" => "3",
logstash_one_channel | "@version" => "1",
logstash_one_channel | "path" => "/usr/share/logstash/input/number2.log",
logstash_one_channel | "@timestamp" => 2019-04-29T14:48:50.589Z
logstash_one_channel | }
logstash_one_channel | {
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hello Habr",
logstash_one_channel | "message" => "4",
logstash_one_channel | "@version" => "1",
logstash_one_channel | "path" => "/usr/share/logstash/input/number1.log",
logstash_one_channel | "@timestamp" => 2019-04-29T14:48:50.856Z
logstash_one_channel | }
Hourra ! Tout a été pris en compte.
Mais il faut prĂ©venir de la façon suivante. Si le conteneur avec logstash est supprimĂ© (docker stop logstash_one_channel && docker rm logstash_one_channel), alors rien ne sera pris en compte. Ă l'intĂ©rieur du conteneur, la position du fichier a Ă©tĂ© sauvegardĂ©e jusqu'oĂč il a Ă©tĂ© lu. Si nous le relançons "Ă zĂ©ro", il ne prendra que les nouvelles lignes.
Lecture des fichiers existants
Supposons que nous lançons logstash pour la premiÚre fois, mais que nous avons déjà des logs que nous aimerions traiter.
Si nous lançons logstash avec la section input que nous avons utilisée ci-dessus, nous ne recevrons rien. Seules les nouvelles lignes seront traitées par logstash.
Pour que les lignes des fichiers existants soient prises en compte, il faut ajouter une ligne supplémentaire à la section input :
input {
file {
start_position => "beginning"
path => "/usr/share/logstash/input/*.log"
}
}
Il convient de noter que cela ne s'applique qu'aux nouveaux fichiers que logstash n'a pas encore vus. Pour les fichiers qui ont déjà été détectés par logstash, il a mémorisé leur taille et ne prendra désormais en compte que les nouvelles entrées.
Concentrons-nous d'abord sur la section input. Il y a encore de nombreuses options, mais pour nos expérimentations, cela suffira pour l'instant.
Routage et transformation des données
Essayons de résoudre le problÚme suivant : supposons que nous recevons des messages d'un canal, certains d'entre eux étant informatifs et d'autres des messages d'erreur. Ils se distinguent par une balise. Certains sont INFO, d'autres ERROR.
Nous devons les séparer à la sortie. C'est-à -dire que nous écrivons les messages informatifs dans un canal, et les messages d'erreur dans un autre.
Pour cela, nous passerons de la section input Ă filter et output.
Avec la section filter, nous allons analyser le message entrant, en extrayant un hash (paires clé-valeur) que nous pourrons ensuite manipuler, c'est-à -dire filtrer selon des conditions. Dans la section output, nous sélectionnerons les messages et enverrons chacun dans son propre canal.
Analyse du message avec grok
Pour analyser des chaßnes de texte et en extraire un ensemble de champs, la section filter contient un plugin spécial : grok.
Sans avoir l'intention de donner ici une description détaillée de celui-ci (pour cela, je vous renvoie à ), je vais donner un exemple simple.
Pour cela, il faut se décider sur le format des chaßnes d'entrée. Voici les miennes :
1 INFO message1
2 ERROR message2
C'est-Ă -dire que l'identifiant est en premiĂšre position, suivi de INFO/ERROR, puis d'un mot sans espaces.
Ce n'est pas compliqué, mais cela suffira pour comprendre le principe de fonctionnement.
Ainsi, dans la section filter, dans le plugin grok, nous devons définir un motif pour analyser nos chaßnes.
Cela ressemblera Ă ceci :
filter {
grok {
match => { "message" => ["%{INT:message_id} %{LOGLEVEL:message_type} %{WORD:message_text}"] }
}
}
C'est essentiellement une expression rĂ©guliĂšre. Des motifs prĂ©dĂ©finis tels que INT, LOGLEVEL, WORD sont utilisĂ©s. Leur description, ainsi que d'autres motifs, peut ĂȘtre consultĂ©e ici
Maintenant, en passant par ce filtre, notre chaĂźne se transformera en un hash de trois champs : message_id, message_type, message_text.
C'est précisément ceux-ci qui seront affichés dans la section output.
Routage des messages dans la section output Ă l'aide de la commande if
Dans la section output, comme nous l'avons vu, nous avions l'intention de séparer les messages en deux flux. Les messages INFO seront affichés dans la console, tandis que les erreurs seront écrites dans un fichier.
Comment devons-nous séparer ces messages ? L'énoncé de la tùche nous suggÚre déjà une solution : nous avons déjà un champ message_type, qui ne peut prendre que deux valeurs INFO et ERROR. C'est sur cette base que nous allons procéder avec l'opérateur if.
if [message_type] == "ERROR" {
# Ici, nous écrivons dans un fichier
} else
{
# Ici, nous écrivons dans stdout
}
La description du fonctionnement avec les champs et les opĂ©rateurs peut ĂȘtre consultĂ©e dans cette section .
Maintenant, parlons de la sortie elle-mĂȘme.
Sortie dans la console, ici tout est clair â stdout {}
Quant Ă la sortie dans un fichier â rappelons-nous que nous exĂ©cutons tout cela depuis un conteneur et que pour que le fichier dans lequel nous Ă©crivons le rĂ©sultat soit accessible de l'extĂ©rieur, nous devons ouvrir ce rĂ©pertoire dans docker-compose.yml.
Total :
La section output de notre fichier ressemble Ă ceci :
output {
if [message_type] == "ERROR" {
file {
path => "/usr/share/logstash/output/test.log"
codec => line { format => "custom format: %{message}"}
}
} else
{stdout {
}
}
}
Dans docker-compose.yml, nous ajoutons un autre volume pour la sortie :
version: '3'
networks:
elk:
volumes:
elasticsearch:
driver: local
services:
logstash:
container_name: logstash_one_channel
image: docker.elastic.co/logstash/logstash:6.3.2
networks:
- elk
environment:
XPACK_MONITORING_ENABLED: "false"
ports:
- 5046:5046
volumes:
- ./config/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
- ./config/pipelines:/usr/share/logstash/config/pipelines:ro
- ./logs:/usr/share/logstash/input
- ./output:/usr/share/logstash/output
Nous lançons, nous testons, nous voyons la séparation en deux flux.
Source : habr.com
