Einführung
Bei der Bereitstellung eines weiteren Systems standen wir vor der Notwendigkeit, eine große Menge an verschiedenen Logs zu verarbeiten. Als Werkzeug wählten wir ELK. In diesem Artikel berichten wir über unsere Erfahrungen mit der Einrichtung dieses Stacks.
Wir beabsichtigen nicht, alle Möglichkeiten zu beschreiben, sondern möchten uns auf die Lösung praktischer Aufgaben konzentrieren. Dies liegt daran, dass es eine Vielzahl von Dokumentationen und bereits vorbereiteten Images gibt, jedoch genügend Fallstricke vorhanden sind, die wir zumindest bei uns festgestellt haben.
Wir haben den Stack über docker-compose bereitgestellt. Darüber hinaus hatten wir eine gut geschriebene docker-compose.yml, die es uns ermöglichte, den Stack nahezu problemlos hochzufahren. Es schien uns, dass der Sieg bereits nahe war; jetzt mussten wir ihn nur noch an unsere Bedürfnisse anpassen, und alles wäre gut.
Leider war der Versuch, das System zur Annahme und Verarbeitung von Logs aus unserer Anwendung zu konfigurieren, zunächst nicht erfolgreich. Daher entschieden wir uns, jeden einzelnen Bestandteil genauer zu untersuchen und danach zu deren Verknüpfungen zurückzukehren.
Also begannen wir mit logstash.
Umgebung, Bereitstellung, Ausführung von Logstash im Container
Für das Deployment verwenden wir docker-compose, die beschriebenen Experimente wurden unter MacOS und Ubuntu 18.0.4 durchgeführt.
Das in unserer ursprünglichen docker-compose.yml angegebene Logstash-Image ist docker.elastic.co/logstash/logstash:6.3.2.
Dieses werden wir für unsere Experimente verwenden.
Für den Start von Logstash haben wir eine separate docker-compose.yml geschrieben. Man hätte das Image natürlich auch über die Kommandozeile starten können, aber wir haben uns auf eine bestimmte Aufgabe konzentriert, bei der alles über docker-compose gestartet wird.
Kurz zu den Konfigurationsdateien
Wie aus der Beschreibung hervorgeht, kann Logstash entweder für einen einzelnen Kanal gestartet werden, in diesem Fall muss ihm eine *.conf-Datei übergeben werden, oder für mehrere Kanäle, wobei ihm eine pipelines.yml übergeben werden muss, die wiederum auf die .conf-Dateien für jeden Kanal verweist.
Wir haben uns für den zweiten Weg entschieden. Dieser erschien uns universeller und skalierbarer. Daher haben wir eine pipelines.yml erstellt und ein Verzeichnis pipelines angelegt, in das wir die .conf-Dateien für jeden Kanal legen werden.
Im Container gibt es eine weitere Konfigurationsdatei — logstash.yml. Diese lassen wir unangetastet und verwenden sie so wie sie ist.
Somit sieht die Struktur unserer Verzeichnisse folgendermaßen aus:

Für die Eingabedaten nehmen wir vorerst an, dass es sich um TCP über Port 5046 handelt, und die Ausgabe erfolgt über stdout.
Das ist eine einfache Konfiguration für den ersten Start. Schließlich besteht die ursprüngliche Aufgabe darin, zu starten.
Hier ist unsere docker-compose.yml-Datei:
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
Was sehen wir hier?
- Networks und Volumes wurden aus der ursprünglichen docker-compose.yml (die, in der der gesamte Stack gestartet wird) übernommen und ich denke, sie haben keinen großen Einfluss auf das Gesamtbild.
- Wir erstellen einen Dienst (services) namens logstash aus dem Image docker.elastic.co/logstash/logstash:6.3.2 und geben ihm den Namen logstash_one_channel.
- Wir leiten den Port 5046 innerhalb des Containers auf denselben internen Port weiter.
- Wir verknüpfen unsere Kanaleinstellungsdatei ./config/pipelines.yml mit der Datei /usr/share/logstash/config/pipelines.yml innerhalb des Containers, wo sie von logstash erkannt wird, und machen sie nur zum Lesen, nur für den Fall.
- Wir zeigen das Verzeichnis ./config/pipelines an, in dem sich unsere Konfigurationsdateien für die Kanäle befinden, im Verzeichnis /usr/share/logstash/config/pipelines und machen es auch schreibgeschützt.

Die Datei pipelines.yml
- pipeline.id: HABR
pipeline.workers: 1
pipeline.batch.size: 1
path.config: "./config/pipelines/habr_pipeline.conf"
Hier wird ein Kanal mit der ID HABR und dem Pfad zu seiner Konfigurationsdatei beschrieben.
Und letztendlich die Datei "./config/pipelines/habr_pipeline.conf"
input {
tcp {
port => "5046"
}
}
filter {
mutate {
add_field => [ "habra_field", "Hello Habr" ]
}
}
output {
stdout {
}
}
Lassen Sie uns zunächst nicht auf die Beschreibung eingehen, sondern versuchen, es zu starten:
docker-compose up
Was sehen wir?
Der Container wurde gestartet. Wir können seine Funktionalität überprüfen:
echo '13123123123123123123123213123213' | nc localhost 5046
Und wir sehen die Antwort in der Konsole des Containers:

Aber gleichzeitig sehen wir auch:
logstash_one_channel | [2019-04-29T11:28:59,790][ERROR][logstash.licensechecker.licensereader] Lizenzinformationen konnten nicht vom Lizenzserver abgerufen werden {:message=>"Elasticsearch Unreachable: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch", …
logstash_one_channel | [2019-04-29T11:28:59,894][INFO ][logstash.pipeline ] Pipeline wurde erfolgreich gestartet {:pipeline_id=>".monitoring-logstash", :thread=>"#"}
logstash_one_channel | [2019-04-29T11:28:59,988][INFO ][logstash.agent ] Pipelines laufen {: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 ist auf Logstash installiert, aber nicht auf Elasticsearch. Bitte installieren Sie X-Pack auf Elasticsearch, um die Überwachungsfunktion nutzen zu können. Andere Funktionen könnten verfügbar sein.
logstash_one_channel | [2019-04-29T11:29:00,526][INFO ][logstash.agent ] Logstash-API-Endpunkt erfolgreich gestartet {:port=>9600}
logstash_one_channel | [2019-04-29T11:29:04,478][INFO ][logstash.outputs.elasticsearch] Führe einen Gesundheitscheck durch, um zu prüfen, ob die Verbindung zu Elasticsearch funktioniert {:healthcheck_url=>http://elasticsearch:9200/, :path=>"/"}
logstash_one_channel | [2019-04-29T11:29:04,487][WARN ][logstash.outputs.elasticsearch] Versuch, die Verbindung zu einer nicht erreichbaren ES-Instanz wiederherzustellen, aber es trat ein Fehler auf. {:url=>«:9200/», :error_type=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=>«Elasticsearch nicht erreichbar: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
logstash_one_channel | [2019-04-29T11:29:04,704][INFO ][logstash.licensechecker.licensereader] Führe einen Gesundheitscheck durch, um zu prüfen, ob die Verbindung zu Elasticsearch funktioniert {:healthcheck_url=>http://elasticsearch:9200/, :path=>"/"}
logstash_one_channel | [2019-04-29T11:29:04,710][WARN ][logstash.licensechecker.licensereader] Versuch, die Verbindung zu einer nicht erreichbaren ES-Instanz wiederherzustellen, aber es trat ein Fehler auf. {:url=>«:9200/», :error_type=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=>«Elasticsearch nicht erreichbar: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
Und unser Protokoll bewegt sich ständig nach oben.
Hier habe ich die erfolgreiche Pipeline-Startmeldung grün, die Fehlermeldung rot und den Versuch, mit gelb zu kommunizieren, hervorgehoben. :9200.
Dies liegt daran, dass in der logstash.conf, die im Bild enthalten ist, eine Überprüfung der Erreichbarkeit von Elasticsearch vorhanden ist. Logstash geht davon aus, dass es Teil des ELK-Stacks läuft, während wir es davon getrennt haben.
Man kann arbeiten, aber es ist nicht bequem.
Die Lösung besteht darin, diese Überprüfung über die Umgebungsvariable XPACK_MONITORING_ENABLED zu deaktivieren.
Wir werden die Änderung in der docker-compose.yml vornehmen und erneut starten:
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
Jetzt funktioniert alles gut. Der Container ist bereit für Experimente.
Wir können erneut in der benachbarten Konsole eingeben:
echo '13123123123123123123123213123213' | nc localhost 5046
Und sehen:
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" => "Hallo Habr",
logstash_one_channel | "host" => "gateway",
logstash_one_channel | "port" => 49418
logstash_one_channel | }
Arbeiten innerhalb eines Kanals
Also, wir sind gestartet. Jetzt können wir uns der Einrichtung von Logstash widmen. Lassen wir die Datei pipelines.yml zunächst außen vor und schauen, was wir mit einem einzigen Kanal erreichen können.
Es ist erwähnenswert, dass das allgemeine Prinzip der Arbeit mit der Konfigurationsdatei des Kanals gut im offiziellen Handbuch beschrieben ist, hier:
Wenn Sie auf Deutsch lesen möchten, haben wir diesen Artikel verwendet: (aber die Syntax der Abfragen dort ist veraltet, das sollte man beachten).
Lassen Sie uns Schritt für Schritt von der Input-Sektion beginnen. Die Arbeit über TCP haben wir bereits gesehen. Was könnte hier noch interessant sein?
Testnachrichten mit Heartbeat
Es gibt eine interessante Möglichkeit, automatische Testnachrichten zu generieren.
Dafür muss im Input-Bereich das Plugin heartbean aktiviert werden.
input {
heartbeat {
message => "HeartBeat!"
}
}
Wir aktivieren es und beginnen, einmal pro Minute zu erhalten.
logstash_one_channel | {
logstash_one_channel | "@timestamp" => 2019-04-29T13:52:04.567Z,
logstash_one_channel | "habra_field" => "Hallo Habr",
logstash_one_channel | "message" => "HeartBeat!",
logstash_one_channel | "@version" => "1",
logstash_one_channel | "host" => "a0667e5c57ec"
logstash_one_channel | }
Wir möchten häufiger empfangen, daher müssen wir den Parameter interval hinzufügen.
So werden wir alle 10 Sekunden eine Nachricht erhalten.
input {
heartbeat {
message => "HeartBeat!"
interval => 10
}
}
Daten aus der Datei abrufen
Wir wollten auch den Datei-Modus betrachten. Wenn es normal mit der Datei funktioniert, benötigen wir möglicherweise keinen Agenten, zumindest nicht für die lokale Nutzung.
Laut Beschreibung sollte der Betriebsmodus dem von tail -f ähnlich sein, d.h. es liest neue Zeilen oder, als Option, liest die gesamte Datei.
Was wir also erreichen möchten:
- Wir möchten die Zeilen erhalten, die in eine Log-Datei geschrieben werden.
- Wir möchten Daten empfangen, die in mehrere Log-Dateien geschrieben werden, und dabei die Möglichkeit haben, zu unterscheiden, woher die Daten stammen.
- Wir möchten prüfen, ob logstash beim Neustart diese Daten nicht erneut erhält.
- Wir möchten sicherstellen, dass, wenn logstash deaktiviert ist und weiterhin Daten in die Dateien geschrieben werden, wir diese Daten beim Neustart erhalten.
Für das Experiment fügen wir eine weitere Zeile in die docker-compose.yml hinzu, indem wir das Verzeichnis öffnen, in das wir die Dateien legen.
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
Und ändern wir den Abschnitt input in habr_pipeline.conf
input {
file {
path => "/usr/share/logstash/input/*.log"
}
}
Lass uns starten:
docker-compose up
Um Logdateien zu erstellen und zu schreiben, verwenden wir den Befehl:
echo '1' >> logs/number1.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hallo 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 | }
Aha, es funktioniert!
Dabei sehen wir, dass das Feld path automatisch hinzugefügt wurde. Das bedeutet, dass wir künftig nach diesem Feld die Einträge filtern können.
Versuchen wir es noch einmal:
echo '2' >> logs/number1.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hallo 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 | }
Und jetzt in eine andere Datei:
echo '1' >> logs/number2.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hallo 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! Die Datei wurde erkannt, der Pfad wurde korrekt angegeben, alles ist gut.
Lassen Sie uns logstash stoppen und neu starten. Warten wir. Ruhe. Das heißt, diese Einträge erhalten wir nicht wieder.
Und jetzt das gewagteste Experiment.
Wir legen logstash ab und führen aus:
echo '3' >> logs/number2.log
echo '4' >> logs/number1.log
Wir starten logstash erneut und sehen:
logstash_one_channel | {
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Hallo 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" => "Hallo 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 | }
Hurra! Alles wurde erkannt.
Es ist jedoch wichtig, folgendes zu beachten: Wenn der Container mit Logstash entfernt wird (docker stop logstash_one_channel && docker rm logstash_one_channel), wird nichts erfasst. Innerhalb des Containers wurde die Position der Datei gespeichert, bis zu der sie gelesen wurde. Wenn man von „Null“ aus startet, werden nur neue Zeilen angenommen.
Lesen bereits existierender Dateien
Angenommen, wir starten Logstash zum ersten Mal, aber wir haben bereits Protokolle und möchten diese verarbeiten.
Wenn wir Logstash mit dem oben verwendeten Input-Abschnitt starten, werden wir nichts erhalten. Nur neue Zeilen werden von Logstash verarbeitet.
Um Zeilen aus bestehenden Dateien einzubeziehen, muss in den Input-Abschnitt eine zusätzliche Zeile hinzugefügt werden:
input {
file {
start_position => "beginning"
path => "/usr/share/logstash/input/*.log"
}
}
Es gibt jedoch einen Haken: Dies funktioniert nur für neue Dateien, die Logstash noch nicht gesehen hat. Für Dateien, die bereits im Blickfeld von Logstash waren, hat es bereits deren Größe gespeichert und wird nun nur neue Einträge daraus berücksichtigen.
Lassen Sie uns an dieser Stelle zum Input-Abschnitt übergehen. Es gibt noch viele weitere Optionen, aber für unsere weiteren Experimente reicht das vorerst.
Datenrouting und -transformation
Lassen Sie uns die folgende Aufgabe lösen: Angenommen, wir erhalten Nachrichten aus einem Kanal. Ein Teil davon sind Informationsmeldungen und der andere Teil sind Fehlermeldungen. Sie unterscheiden sich durch ihre Tags: einige sind INFO, andere ERROR.
Wir müssen sie am Ausgang aufteilen. Das heißt, die Informationsmeldungen schreiben wir in einen Kanal und die Fehlermeldungen in einen anderen.
Dazu wechseln wir von der Sektion Input zu Filter und Output.
Mit der Sektion Filter analysieren wir die eingehende Nachricht und erhalten daraus einen Hash (Schlüssel-Wert-Paar), mit dem wir arbeiten können, also nach Bedingungen sortieren. In der Sektion Output wählen wir die Nachrichten aus und senden jede in ihren eigenen Kanal.
Die Analyse von Nachrichten mit Grok
Um Textzeilen zu analysieren und eine Reihe von Feldern daraus zu extrahieren, gibt es in der Sektion Filter ein spezielles Plugin – Grok.
Ohne das Ziel zu verfolgen, hier eine detaillierte Beschreibung davon zu geben (dafür verweisen ich auf ), gebe ich ein einfaches Beispiel.
Dafür müssen wir uns über das Format der Eingangsnachrichten klar werden. Meine sehen folgendermaßen aus:
1 INFO message1
2 ERROR message2
Das heißt, die Identifikationsnummer steht an erster Stelle, gefolgt von INFO/ERROR und dann einem beliebigen Wort ohne Leerzeichen.
Nicht kompliziert, aber für das Verständnis des Arbeitsprinzips reicht das.
Im Abschnitt 'filter' des Grok-Plugins müssen wir ein Muster zur Analyse unserer Zeilen festlegen.
Es wird folgendermaßen aussehen:
filter {
grok {
match => { "message" => ["%{INT:message_id} %{LOGLEVEL:message_type} %{WORD:message_text}"] }
}
}
Im Grunde handelt es sich um einen regulären Ausdruck. Es werden bereits vorhandene Muster wie INT, LOGLEVEL, WORD verwendet. Ihre Beschreibung sowie andere Muster können hier eingesehen werden:
Wenn wir diesen Filter anwenden, wird unsere Zeile in einen Hash mit drei Feldern umgewandelt: message_id, message_type, message_text.
Diese werden im Abschnitt 'output' ausgegeben.
Nachrichtenweiterleitung im Abschnitt 'output' mit der if-Anweisung.
Im Abschnitt 'output' beabsichtigten wir, die Nachrichten in zwei Ströme zu unterteilen. Die einen – die INFO-Nachrichten – werden wir auf der Konsole ausgeben, die mit Fehlern speichern wir in einer Datei.
Wie teilen wir diese Nachrichten auf? Die Aufgabenstellung gibt bereits einen Hinweis auf die Lösung – wir haben ein spezifisches Feld message_type, das nur zwei Werte annehmen kann: INFO und ERROR. Darauf basierend verwenden wir den if-Operator zur Auswahl.
if [message_type] == "ERROR" {
# Hier in die Datei ausgeben
} else
{
# Hier in stdout ausgeben
}
Eine Beschreibung zum Umgang mit Feldern und Operatoren kann in diesem Abschnitt eingesehen werden: .
Nun, kommen wir zum eigentlichen Output.
Die Ausgabe in die Konsole ist klar — stdout {}
Die Ausgabe in eine Datei ist etwas anders — wir erinnern uns, dass wir dies aus einem Container heraus starten. Damit die Datei, in die wir das Ergebnis schreiben, von außen zugänglich ist, müssen wir dieses Verzeichnis in docker-compose.yml freigeben.
Insgesamt:
Der Abschnitt output unserer Datei sieht folgendermaßen aus:
output {
if [message_type] == "ERROR" {
file {
path => "/usr/share/logstash/output/test.log"
codec => line { format => "custom format: %{message}"}
}
} else
{stdout {
}
}
}
In docker-compose.yml fügen wir ein weiteres Volume für die Ausgabe hinzu:
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
Wir starten, probieren es aus und sehen die Aufteilung in zwei Ströme.
Quelle: habr.com
