Inleiding
Tijdens het opzetten van een nieuw systeem stuitten we op de noodzaak om een groot aantal verschillende logs te verwerken. We kozen voor ELK als onze tool. In dit artikel bespreken we onze ervaringen met het configureren van deze stack.
We hebben niet de ambitie om al zijn mogelijkheden te beschrijven, maar willen ons richten op het oplossen van praktische problemen. Dit is te wijten aan het feit dat er een overvloed aan documentatie en kant-en-klare afbeeldingen beschikbaar zijn, maar er zijn genoeg valkuilen, althans dat hebben wij ontdekt.
We hebben de stack opgezet via docker-compose. Bovendien hadden we een goed geschreven docker-compose.yml, waarmee we de stack vrijwel probleemloos konden opzetten. We dachten dat de overwinning al nabij was; we moesten het alleen nog een beetje afstemmen op onze behoeften.
Helaas was onze poging om het systeem aan te passen voor het ontvangen en verwerken van logs van onze applicatie aanvankelijk niet succesvol. Daarom besloten we elk onderdeel afzonderlijk te bestuderen voordat we terugkeerden naar hun onderlinge verbanden.
Laten we beginnen met logstash.
Omgeving, implementatie, het starten van Logstash in een container
Voor de implementatie gebruiken we docker-compose; de experimenten die hier worden beschreven, werden uitgevoerd op MacOS en Ubuntu 18.0.4.
Het logstash-image dat we in ons oorspronkelijke docker-compose.yml hebben geconfigureerd, is docker.elastic.co/logstash/logstash:6.3.2
Dit zullen we gebruiken voor onze experimenten.
Voor het starten van logstash hebben we een aparte docker-compose.yml geschreven. Natuurlijk hadden we het image ook vanaf de commandoregel kunnen starten, maar we hadden een specifieke taak waarvoor alles via docker-compose werd uitgevoerd.
Kort over configuratiebestanden
Zoals uit de beschrijving blijkt, kan logstash worden uitgevoerd voor één kanaal, in dat geval moet een *.conf-bestand worden doorgegeven, of voor meerdere kanalen, dan moet een pipelines.yml-bestand worden doorgegeven, dat op zijn beurt zal verwijzen naar .conf-bestanden voor elk kanaal.
We kozen voor de tweede optie. Dit leek ons universeler en schaalbaarder. Daarom hebben we een pipelines.yml gemaakt en een directory voor pipelines opgezet, waarin we de .conf-bestanden voor elk kanaal zullen plaatsen.
Binnen de container is er nog een configuratiebestand — logstash.yml. We raken dit niet aan en gebruiken het zoals het is.
De structuur van onze mappen is als volgt:

Voor het verkrijgen van inputgegevens gaan we ervan uit dat het tcp via poort 5046 is, en voor uitvoer gebruiken we stdout.
Dit is een eenvoudige configuratie voor de eerste uitvoering. Want de initiële taak is om het te starten.
Dus, we hebben zo'n 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
Wat zien we hier?
- Networks en volumes zijn overgenomen uit de oorspronkelijke docker-compose.yml (die waar de volledige stack wordt gestart) en ik denk dat ze niet sterk van invloed zijn op het totaalplaatje.
- We creëren één service (services) logstash, uit de afbeelding docker.elastic.co/logstash/logstash:6.3.2 en geven deze de naam logstash_one_channel.
- We brengen poort 5046 naar binnen in de container, op dezelfde interne poort.
- We koppelen ons configuratiebestand ./{config/pipelines.yml aan het bestand /usr/share/logstash/config/pipelines.yml binnen de container, waar logstash het zal oppakken en we maken het alleen-lezen, gewoon voor de zekerheid.
- We koppelen de map ./{config/pipelines, waar onze bestanden met kanaalinstellingen staan, aan de map /usr/share/logstash/config/pipelines en maken deze ook alleen-lezen.

Bestand pipelines.yml
- pipeline.id: HABR
pipeline.workers: 1
pipeline.batch.size: 1
path.config: ".{config/pipelines/habr_pipeline.conf"
Hier wordt één kanaal beschreven met de identificatie HABR en het pad naar zijn configuratiebestand.
En eindelijk het bestand ".{config/pipelines/habr_pipeline.conf"
input {
tcp {
port => "5046"
}
}
filter {
mutate {
add_field => [ "habra_field", "Hallo Habr" ]
}
}
output {
stdout {
}
}
Laten we nog niet in zijn beschrijving duiken, laten we proberen het te starten:
docker-compose up
Wat zien we?
De container is gestart. We kunnen de werking ervan controleren:
echo '13123123123123123123123213123213' | nc localhost 5046
En we zien het antwoord in de console van de container:

Maar we zien ook:
logstash_one_channel | [2019-04-29T11:28:59,790][ERROR][logstash.licensechecker.licensereader] Kunnen geen licentie-informatie ophalen van de licentieserver {:message=>"Elasticsearch niet bereikbaar: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch", …
logstash_one_channel | [2019-04-29T11:28:59,894][INFO ][logstash.pipeline ] Pipeline met succes gestart {:pipeline_id=>".monitoring-logstash", :thread=>"#"}
logstash_one_channel | [2019-04-29T11:28:59,988][INFO ][logstash.agent ] Pipelines draaien {: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 is geïnstalleerd op Logstash maar niet op Elasticsearch. Installeer X-Pack op Elasticsearch om de monitoringfunctie te gebruiken. Andere functies zijn misschien beschikbaar.
logstash_one_channel | [2019-04-29T11:29:00,526][INFO ][logstash.agent ] Logstash API-eindpunt succesvol gestart {:port=>9600}
logstash_one_channel | [2019-04-29T11:29:04,478][INFO ][logstash.outputs.elasticsearch] Gezondheidscontrole uitvoeren om te zien of een Elasticsearch-verbinding werkt {:healthcheck_url=http://elasticsearch:9200/, :path="/"}
logstash_one_channel | [2019-04-29T11:29:04,487][WARN ][logstash.outputs.elasticsearch] Poging gedaan om verbinding te herstellen met dode ES-instantie, maar kreeg een foutmelding. {:url=«:9200/», :error_type=LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=«Elasticsearch onbereikbaar: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
logstash_one_channel | [2019-04-29T11:29:04,704][INFO ][logstash.licensechecker.licensereader] Gezondheidscontrole uitvoeren om te zien of een Elasticsearch-verbinding werkt {:healthcheck_url=http://elasticsearch:9200/, :path="/"}
logstash_one_channel | [2019-04-29T11:29:04,710][WARN ][logstash.licensechecker.licensereader] Poging gedaan om verbinding te herstellen met dode ES-instantie, maar kreeg een foutmelding. {:url=«:9200/», :error_type=LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=«Elasticsearch onbereikbaar: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
En onze log groeit de hele tijd omhoog.
Hier heb ik de boodschap in het groen gemarkeerd dat de pipeline succesvol is gestart, in het rood — de foutmelding en in het geel — de boodschap over de poging om contact te maken met :9200.
Dit gebeurt omdat er in logstash.conf, inbegrepen in de afbeelding, een controle is op de beschikbaarheid van Elasticsearch. Logstash gaat ervan uit dat het deel uitmaakt van de Elk-stack, en wij hebben het gescheiden.
Werken is mogelijk, maar niet handig.
De oplossing is om deze controle te deactiveren via de omgevingsvariabele XPACK_MONITORING_ENABLED.
Laten we de wijziging aanbrengen in docker-compose.yml en opnieuw 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
Nu is alles goed. De container is klaar voor experimenten.
We kunnen opnieuw invoeren in de buurconsole:
echo '13123123123123123123123213123213' | nc localhost 5046
En zien:
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 | }
Werken in één kanaal
Dus, we zijn gestart. Nu kunnen we daadwerkelijk tijd besteden aan de configuratie van logstash zelf. Laten we het bestand pipelines.yml voorlopig met rust laten en kijken wat we kunnen bereiken met één kanaal.
Het moet gezegd worden dat het algemene principe van het werken met het configuratiebestand van het kanaal goed beschreven is in de officiële handleiding, hier
Als je in het Nederlands wilt lezen, hebben we deze (maar de syntaxis van de verzoeken daar is verouderd, dat moet je in aanmerking nemen).
Laten we stap voor stap de sectie Input doorlopen. We hebben het werk via tcp al gezien. Wat kan hier nog interessant zijn?
Testberichten, gebruik makend van heartbeat
Er is een interessante mogelijkheid om automatische testberichten te genereren.
Hiervoor moet de heartbeat-plugin in de input-sectie worden ingeschakeld.
input {
heartbeat {
message => "HeartBeat!"
}
}
We schakelen het in en beginnen elk minuut een bericht te ontvangen.
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 | }
We willen vaker ontvangsten, we moeten de parameter interval toevoegen.
Zo ontvangen we elk 10 seconden een bericht.
input {
heartbeat {
message => "HeartBeat!"
interval => 10
}
}
Gegevens ophalen uit een bestand
We hebben ook besloten om de bestandsmodus te bekijken. Als het goed werkt met het bestand, is er misschien geen agent nodig, althans voor lokaal gebruik.
Volgens de beschrijving zou de bedrijfsmodus vergelijkbaar moeten zijn met tail -f, d.w.z. het leest nieuwe regels of, als optie, leest het hele bestand.
Dus, wat willen we bereiken:
- We willen de regels ontvangen die aan een logbestand worden toegevoegd.
- We willen gegevens ontvangen die in meerdere logbestanden worden geschreven, met de mogelijkheid om te splitsen wat van waar vandaan komt.
- We willen controleren of logstash deze gegevens niet opnieuw ontvangt bij herstart.
- We willen controleren of logstash, als het wordt uitgeschakeld terwijl gegevens in bestanden blijven schrijven, deze gegevens zal ontvangen wanneer het weer wordt gestart.
Voor het experiment voegen we nog een regel toe aan docker-compose.yml, waarbij we de directory openen waar we de bestanden plaatsen.
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
En we zullen de input-sectie in habr_pipeline.conf aanpassen.
input {
file {
path => "/usr/share/logstash/input/*.log"
}
}
We starten op:
docker-compose up
Voor het creëren en opslaan van logbestanden zullen we het volgende commando gebruiken:
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 | }
Ja, het werkt!
Hierbij zien we dat het pad-veld automatisch is toegevoegd. Dus in de toekomst kunnen we records filteren op basis van dit veld.
Laten we het nog een keer proberen:
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 | }
En nu in een ander bestand:
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 | }
Uitstekend! Het bestand is opgepikt, het pad is correct aangegeven, alles is goed.
Laten we logstash stoppen en opnieuw starten. Laten we wachten. Stilte. Dat wil zeggen, we krijgen deze records niet opnieuw.
En nu de meest gewaagde experiment.
Laten we logstash stoppen en uitvoeren:
echo '3' >> logs/number2.log
echo '4' >> logs/number1.log
We starten logstash opnieuw en zien:
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 | }
Hoera! Alles is opgepikt.
Maar, ik moet je waarschuwen voor het volgende. Als de container met logstash wordt verwijderd (docker stop logstash_one_channel && docker rm logstash_one_channel), dan wordt er niets opgepikt. Binnen de container was de positie van het bestand bewaard, tot waar het was gelezen. Als je vanaf 'nul' begint, accepteert het alleen nieuwe regels.
Het lezen van al bestaande bestanden
Stel dat we logstash voor de eerste keer starten, maar we hebben al logs en we willen deze verwerken.
Als we logstash starten met de inputsectie die we hierboven hebben gebruikt, zullen we niets ontvangen. Alleen nieuwe regels worden door logstash verwerkt.
Om bestaande regels uit de bestaande bestanden te laten doorstromen, moet in de inputsectie een extra regel worden toegevoegd:
input {
file {
start_position => "beginning"
path => "/usr/share/logstash/input/*.log"
}
}
Er is echter een nuance, dit werkt alleen voor nieuwe bestanden die logstash nog niet heeft gezien. Voor bestanden die logstash al eerder heeft gezien, heeft het hun grootte al onthouden en zullen alleen nieuwe records worden genomen.
Laten we hier bij de studie van de inputsectie blijven. Er zijn veel meer opties, maar voor onze verdere experimenten is dit voorlopig voldoende.
Routering en gegevensconversie
Laten we proberen het volgende probleem op te lossen; stel dat we berichten uit één kanaal krijgen, waarvan een deel informatieberichten zijn en een deel foutmeldingen. Ze verschillen door hun tag. Sommige zijn INFO, andere zijn ERROR.
We moeten ze aan het einde scheiden. Dus, informatieberichten schrijven we naar één kanaal, en foutmeldingen naar een ander.
Hiervoor gaan we van de inputsectie naar filter en output.
Met de filtersectie zullen we het inkomende bericht analyseren en daar een hash (sleutel-waardeparen) uit halen waarmee we kunnen werken, dus kunnen we deze op voorwaarden splitsen. In de outputsectie selecteren we de berichten en sturen we elk naar zijn eigen kanaal.
Berichtanalyse met grok
Om tekstregels te analyseren en sets van velden te verkrijgen, is er in de filtersectie een speciale plugin genaamd grok.
Zonder de bedoeling te hebben om hier een gedetailleerde beschrijving te geven (voor dat verwijs ik naar ), zal ik een eenvoudig voorbeeld geven.
Hiervoor moeten we ons bepalen tot het formaat van de inkomende regels. Die van mij zijn als volgt:
1 INFO message1
2 ERROR message2
Dus, de identifier staat op de eerste plaats, daarna INFO/ERROR, en dan een woord zonder spaties.
Niet moeilijk, maar voldoende om het principe van werking te begrijpen.
Dus, in de filtersectie, in de grok-plugin, moeten we een patroon definiëren voor de analyse van onze regels.
Zo zou het eruitzien:
filter {
grok {
match => { "message" => ["%{INT:message_id} %{LOGLEVEL:message_type} %{WORD:message_text}"] }
}
}
In wezen is dit een reguliere expressie. Er worden al bestaande patronen gebruikt, zoals INT, LOGLEVEL, WORD. De beschrijving ervan, evenals andere patronen, kun je hier bekijken.
Nu, als we door deze filter gaan, zal onze regel worden omgezet in een hash van drie velden: message_id, message_type, message_text.
Zij zullen worden weergegeven in de outputsectie.
Berichtroutering in de outputsectie met de if-opdracht
In de outputsectie, zoals we ons herinneren, zouden we de berichten in twee stromen splitsen. De ene - die INFO is, gaan we naar de console sturen, en de berichten met fouten zullen we in een bestand schrijven.
Hoe splitsen we deze berichten? De opgave suggereert al een oplossing - we hebben immers al het veld message_type, dat slechts twee waarden kan aannemen: INFO en ERROR. Dit veld gaan we gebruiken voor de keuze met de if-operator.
if [message_type] == "ERROR" {
# Hier schrijven we naar het bestand
} else
{
# Hier schrijven we naar stdout
}
De beschrijving van het werken met velden en operators kan in deze sectie worden bekeken .
Nu, over de daadwerkelijke output.
Output naar de console, hier is alles duidelijk - stdout {}
Maar de output naar een bestand - we herinneren ons dat we dit alles vanuit een container draaien en zodat het bestand waarin we het resultaat schrijven toegankelijk is van buitenaf, moeten we deze map openen in docker-compose.yml.
Kortom:
De outputsectie van ons bestand ziet er als volgt uit:
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 voegen we nog een volume toe voor de output:
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
We starten het, proberen het en zien de scheiding in twee stromen.
Bron: habr.com
