Introduzione
Nel configurare un nuovo sistema, ci siamo trovati di fronte alla necessità di elaborare un grande numero di log diversi. Come strumento abbiamo scelto ELK. In questo articolo parleremo della nostra esperienza nella configurazione di questo stack.
Non abbiamo l'obiettivo di descrivere tutte le sue capacità, ma vogliamo concentrarci sulla risoluzione di compiti pratici. Questo è dovuto al fatto che, nonostante ci sia una quantità considerevole di documentazione e immagini già pronte, ci sono abbastanza insidie, almeno noi le abbiamo scoperte.
Abbiamo distribuito lo stack tramite docker-compose. Inoltre, avevamo un docker-compose.yml ben scritto che ci ha permesso di sollevare lo stack quasi senza problemi. E ci sembrava che la vittoria fosse già vicina, bastava adattarlo un po' alle nostre esigenze e il gioco era fatto.
Sfortunatamente, il tentativo di adattare il sistema per ricevere ed elaborare i log dalla nostra applicazione non ha avuto successo subito. Pertanto, abbiamo deciso che valeva la pena studiare ogni componente separatamente e poi tornare ai loro collegamenti.
Iniziamo quindi con Logstash.
Ambiente, distribuzione, avvio di Logstash in un contenitore
Per la distribuzione utilizziamo docker-compose; gli esperimenti descritti qui sono stati condotti su MacOS e Ubuntu 18.0.4.
L'immagine di Logstash che avevamo nel nostro docker-compose.yml è docker.elastic.co/logstash/logstash:6.3.2
La utilizzeremo per gli esperimenti.
Per avviare Logstash abbiamo scritto un docker-compose.yml separato. Certo, avremmo potuto avviarlo dalla riga di comando, ma stavamo risolvendo un compito specifico, dove tutto viene avviato tramite docker-compose.
Brevemente sui file di configurazione
Come indicato nella descrizione, Logstash può essere avviato per un solo canale; in questo caso, è necessario fornirgli un file *.conf, oppure per più canali; in questo caso, è necessario fornire un file pipelines.yml, il quale, a sua volta, farà riferimento ai file .conf per ciascun canale.
Abbiamo scelto la seconda opzione. Ci è sembrata più universale e scalabile. Pertanto, abbiamo creato un pipelines.yml e abbiamo fatto una directory pipelines in cui metteremo file .conf per ogni canale.
All'interno del contenitore c'è un altro file di configurazione — logstash.yml. Non lo tocchiamo, lo utilizziamo così com'è.
Quindi, la struttura delle nostre cartelle:

Per ricevere i dati in ingresso, al momento consideriamo che sia tcp sulla porta 5046, e per l'output useremo stdout.
Ecco una configurazione semplice per il primo avvio. Poiché l'obiettivo iniziale è avviare.
Quindi, abbiamo il seguente 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
Cosa vediamo qui?
- Le reti e i volumi sono stati presi dal docker-compose.yml sorgente (quello in cui viene avviato l'intero stack) e penso che non influenzino molto il quadro complessivo.
- Stiamo creando un servizio (services) logstash, dall'immagine docker.elastic.co/logstash/logstash:6.3.2 e gli diamo il nome logstash_one_channel.
- Stiamo instradando la porta 5046 all'interno del contenitore, sulla stessa porta interna.
- Stiamo mappando il nostro file di configurazione dei canali ./config/pipelines.yml sul file /usr/share/logstash/config/pipelines.yml all'interno del contenitore, da cui logstash lo prenderà e lo rendiamo di sola lettura, giusto per precauzione.
- Stiamo mappando la directory ./config/pipelines, dove abbiamo i file di configurazione dei canali, nella directory /usr/share/logstash/config/pipelines e la rendiamo anche di sola lettura.

File pipelines.yml
- pipeline.id: HABR
pipeline.workers: 1
pipeline.batch.size: 1
path.config: "./config/pipelines/habr_pipeline.conf"
Qui è descritto un canale con l'identificatore HABR e il percorso al suo file di configurazione.
E infine il file "./config/pipelines/habr_pipeline.conf"
input {
tcp {
port => "5046"
}
}
filter {
mutate {
add_field => [ "habra_field", "Hello Habr" ]
}
}
output {
stdout {
}
}
Non approfondiamo ancora la sua descrizione, proviamo ad avviarlo:
docker-compose up
Cosa vediamo?
Il contenitore è avviato. Possiamo controllare il suo funzionamento:
echo '13123123123123123123123213123213' | nc localhost 5046
E vediamo nella console del contenitore la risposta:

Ma vediamo anche:
logstash_one_channel | [2019-04-29T11:28:59,790][ERROR][logstash.licensechecker.licensereader] Impossibile recuperare le informazioni sulla licenza dal server di licenza {:message=>"Elasticsearch Unreachable: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch", …
logstash_one_channel | [2019-04-29T11:28:59,894][INFO ][logstash.pipeline ] Pipeline avviato con successo {:pipeline_id=>".monitoring-logstash", :thread=>"#"}
logstash_one_channel | [2019-04-29T11:28:59,988][INFO ][logstash.agent ] Pipeline in esecuzione {: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 è installato su Logstash ma non su Elasticsearch. Installare X-Pack su Elasticsearch per utilizzare la funzione di monitoraggio. Altre funzionalità potrebbero essere disponibili.
logstash_one_channel | [2019-04-29T11:29:00,526][INFO ][logstash.agent ] Endpoint API di Logstash avviato con successo {:port=>9600}
logstash_one_channel | [2019-04-29T11:29:04,478][INFO ][logstash.outputs.elasticsearch] Eseguendo un controllo di integrità per verificare se una connessione Elasticsearch è funzionante {:healthcheck_url=http://elasticsearch:9200/, :path="/"}
logstash_one_channel | [2019-04-29T11:29:04,487][WARN ][logstash.outputs.elasticsearch] Tentativo di ripristinare la connessione a un'istanza ES morta, ma si è verificato un errore. {:url=«:9200/, :error_type=LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=«Elasticsearch non raggiungibile: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
logstash_one_channel | [2019-04-29T11:29:04,704][INFO ][logstash.licensechecker.licensereader] Eseguendo un controllo di integrità per verificare se una connessione Elasticsearch è funzionante {:healthcheck_url=http://elasticsearch:9200/, :path="/"}
logstash_one_channel | [2019-04-29T11:29:04,710][WARN ][logstash.licensechecker.licensereader] Tentativo di ripristinare la connessione a un'istanza ES morta, ma si è verificato un errore. {:url=«:9200/, :error_type=LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=«Elasticsearch non raggiungibile: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
E il nostro log continua a salire.
Qui ho evidenziato in verde il messaggio che indica che il pipeline è stato avviato con successo, in rosso il messaggio di errore e in giallo il messaggio di tentativo di contattare :9200.
Questo accade perché nel file logstash.conf, incluso nell'immagine, è presente un controllo sulla disponibilità di elasticsearch. Infatti, logstash presuppone di operare all'interno del stack Elk, ma noi lo abbiamo separato.
È possibile lavorare, ma non è comodo.
La soluzione è disabilitare questo controllo tramite la variabile ambiente XPACK_MONITORING_ENABLED.
Apportiamo la modifica al docker-compose.yml e riavviamo:
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
Adesso va tutto bene. Il contenitore è pronto per gli esperimenti.
Possiamo digitare di nuovo in una console accanto:
echo '13123123123123123123123213123213' | nc localhost 5046
E vedere:
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" => "Ciao Habr",
logstash_one_channel | "host" => "gateway",
logstash_one_channel | "port" => 49418
logstash_one_channel | }
Lavoro all'interno di un solo canale
Quindi, abbiamo avviato. Ora possiamo dedicare tempo alla configurazione di logstash. Non tocchiamo ancora il file pipelines.yml, vediamo cosa possiamo ottenere lavorando con un solo canale.
Va detto che il principio generale di lavoro con il file di configurazione del canale è ben descritto nella documentazione ufficiale, qui
Se si desidera leggere in russo, abbiamo utilizzato questo (ma la sintassi delle query è vecchia, bisogna tenerne conto).
Andiamo in modo sequenziale dalla sezione Input. Abbiamo già visto il lavoro con tcp. Cos'altro può essere interessante qui?
Messaggi di prova, utilizzando heartbeat
C'è una possibilità interessante di generare messaggi di prova automatici.
Per questo, nella sezione input bisogna abilitare il plugin heartbeat.
input {
heartbeat {
message => "HeartBeat!"
}
}
Attiviamo, iniziamo a ricevere una volta al minuto
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 | }
Vogliamo ricevere più frequentemente, dobbiamo aggiungere il parametro interval.
Così riceveremo un messaggio ogni 10 secondi.
input {
heartbeat {
message => "HeartBeat!"
interval => 10
}
}
Ricezione dei dati da un file
Abbiamo anche deciso di controllare la modalità file. Se funziona bene con il file, potrebbe non essere necessario alcun agente, almeno per l'uso locale.
Secondo la descrizione, la modalità operativa dovrebbe essere simile a tail -f, cioè legge nuove righe o, come opzione, legge l'intero file.
Quindi, cosa vogliamo ottenere:
- Vogliamo ricevere le righe che vengono aggiunte a un file di log.
- Vogliamo ricevere dati che vengono scritti in più file di log, avendo la possibilità di distinguere da quale file proviene ciascun dato.
- Vogliamo verificare che al riavvio di logstash non riceva nuovamente questi dati.
- Vogliamo verificare che se logstash viene disattivato e i dati continuano a essere scritti nei file, quando lo riavviamo, otteniamo questi dati.
Per condurre l'esperimento, aggiungeremo un'altra riga a docker-compose.yml, aprendo la directory in cui mettiamo i file.
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
E modificheremo la sezione input in habr_pipeline.conf
input {
file {
path => "/usr/share/logstash/input/*.log"
}
}
Iniziamo:
docker-compose up
Per creare e scrivere file di log utilizzeremo il comando:
echo '1' >> logs/number1.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Ciao 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 | }
Sì, funziona!
Inoltre, vediamo che è stato aggiunto automaticamente il campo path. Questo significa che in futuro potremo filtrare i record in base ad esso.
Proviamo ancora:
echo '2' >> logs/number1.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Ciao 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 | }
E ora in un altro file:
echo '1' >> logs/number2.log
{
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Ciao 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 | }
Perfetto! Il file è stato acquisito, il path è stato indicato correttamente, tutto bene.
Fermiamo logstash e lo riavviamo. Aspettiamo. Silenzio. Cioè, non riceviamo nuovamente questi record.
E ora l'esperimento più audace.
Mettiamo a terra logstash ed eseguiamo:
echo '3' >> logs/number2.log
echo '4' >> logs/number1.log
Riavviamo logstash e vediamo:
logstash_one_channel | {
logstash_one_channel | "host" => "ac2d4e3ef70f",
logstash_one_channel | "habra_field" => "Ciao 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" => "Ciao 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 | }
Evviva! Tutto è stato acquisito.
Ma devo avvisare riguardo al seguente punto. Se il contenitore con logstash viene rimosso (docker stop logstash_one_channel && docker rm logstash_one_channel), nulla verrà acquisito. All'interno del contenitore è stata salvata la posizione del file fino a dove è stato letto. Se si avvia 'da zero', prenderà solo nuove righe.
Lettura di file esistenti
Supponiamo che stiamo avviando logstash per la prima volta, ma abbiamo già dei log e vorremmo elaborarli.
Se avviamo logstash con la sezione input che abbiamo utilizzato sopra, non otterremo nulla. Solo le nuove righe verranno elaborate da logstash.
Per estrarre le righe dai file esistenti, è necessario aggiungere alla sezione input una riga aggiuntiva:
input {
file {
start_position => "beginning"
path => "/usr/share/logstash/input/*.log"
}
}
Nota che c'è un'intesa: questo funziona solo per i nuovi file che logstash non ha ancora visto. Per i file che logstash ha già analizzato, ricorda la loro dimensione e ora prenderà solo le nuove voci in essi.
Fermiamoci qui per quanto riguarda la sezione input. Ci sono molte altre opzioni, ma per i nostri esperimenti futuri, per ora possono bastare.
Instradamento e trasformazione dei dati
Proviamo a risolvere il seguente problema, supponiamo che riceviamo messaggi da un canale, alcuni di essi informativi e altri segnali di errore. Si distinguono per tag. Alcuni sono INFO, altri ERROR.
Dobbiamo separarli in uscita. Cioè, scriviamo i messaggi informativi in un canale e i messaggi di errore in un altro.
Per fare ciò, passeremo dalla sezione input alla filter e output.
Usando la sezione filter, analizzeremo il messaggio in ingresso, ottenendo un hash (coppie chiave-valore) su cui possiamo lavorare, cioè analizzarlo in base a delle condizioni. E nella sezione output, selezioneremo i messaggi e invieremo ciascuno al proprio canale.
Analisi del messaggio con grok
Per analizzare le righe di testo e ottenere un insieme di campi, durante la sezione filter c'è un plugin speciale — grok.
Senza voler fornire una descrizione dettagliata qui (per questo rimando a ), darò il mio semplice esempio.
Per fare ciò, dobbiamo definirci il formato delle righe in ingresso. Le mie sono così:
1 INFO message1
2 ERROR message2
Cioè, l'identificatore al primo posto, poi INFO/ERROR, poi una parola senza spazi.
Non è difficile, ma per capire il principio di funzionamento è sufficiente.
Quindi, nella sezione filter, nel plugin grok dobbiamo definire un pattern per analizzare le nostre righe.
Dovrebbe apparire così:
filter {
grok {
match => { "message" => ["%{INT:message_id} %{LOGLEVEL:message_type} %{WORD:message_text}"] }
}
}
In sostanza, si tratta di un'espressione regolare. Vengono utilizzati pattern già pronti, come INT, LOGLEVEL, WORD. La loro descrizione, così come altri pattern, può essere vista qui
Ora, passando attraverso questo filtro, la nostra riga si trasformerà in un hash di tre campi: message_id, message_type, message_text.
Questi saranno i campi che verranno visualizzati nella sezione output.
Instradamento dei messaggi nella sezione output utilizzando il comando if
Nella sezione output, come ricordiamo, volevamo separare i messaggi in due flussi. Alcuni - quelli INFO, li visualizzeremo sulla console, mentre gli errori li salveremo in un file.
Come possiamo separare questi messaggi? La condizione del compito suggerisce già la soluzione: abbiamo un campo dedicato message_type, che può assumere solo due valori INFO ed ERROR. Sarà proprio su questo che faremo la selezione utilizzando l'operatore if.
if [message_type] == "ERROR" {
# Qui scriviamo nel file
} else
{
# Qui scriviamo su stdout
}
La descrizione di come lavorare con i campi e gli operatori può essere consultata in questa sezione .
Ora, parliamo dello stesso output.
Output nella console, qui è tutto chiaro - stdout {}
Ma per quanto riguarda l'output in un file, ricordiamo che stiamo eseguendo tutto questo da un contenitore e affinché il file in cui scriviamo i risultati sia accessibile dall'esterno, dobbiamo aprire questa directory in docker-compose.yml.
In totale:
La sezione output del nostro file appare così:
output {
if [message_type] == "ERROR" {
file {
path => "/usr/share/logstash/output/test.log"
codec => line { format => "formato personalizzato: %{message}"}
}
} else
{stdout {
}
}
}
In docker-compose.yml aggiungiamo un altro volume, per l'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
Avviamo, proviamo, vediamo la separazione in due flussi.
Fonte: habr.com
