Introduzione
Durante il lancio di un nuovo sistema, ci siamo trovati nella necessità di gestire un gran numero di log diversi. Come strumento abbiamo scelto ELK. In questo articolo parleremo della nostra esperienza nella configurazione di questo stack.
Non ci poniamo l'obiettivo di descrivere tutte le sue funzionalità, ma vogliamo concentrarci specificamente sulla risoluzione di problemi pratici. Questo è dovuto al fatto che, nonostante ci sia una quantità considerevole di documentazione e immagini già pronte, ci sono molte insidie, almeno le abbiamo scoperte noi.
Abbiamo implementato lo stack tramite docker-compose. Inoltre, avevamo un docker-compose.yml ben scritto che ci ha permesso di sollevare lo stack praticamente senza problemi. E ci sembrava che la vittoria fosse già vicina, bastava un tocco finale per adattarlo alle nostre esigenze e il gioco era fatto.
Sfortunatamente, il tentativo di perfezionare il sistema per ricevere e trattare i log dalla nostra applicazione non ha avuto successo immediato. Quindi, abbiamo deciso che fosse opportuno studiare ogni componente separatamente per poi tornare alle loro interazioni.
Quindi, abbiamo iniziato con logstash.
Ambiente, implementazione, avvio di Logstash in un contenitore
Per il deployment utilizziamo docker-compose; gli esperimenti descritti qui sono stati effettuati su MacOS e Ubuntu 18.0.4.
L'immagine di logstash, che abbiamo definito nel nostro file docker-compose.yml, è docker.elastic.co/logstash/logstash:6.3.2
La utilizzeremo per gli esperimenti.
Per avviare logstash abbiamo scritto un file docker-compose.yml separato. Certo, avremmo potuto lanciare l'immagine da riga di comando, ma stavamo affrontando un compito specifico in cui tutto viene avviato tramite docker-compose.
In breve sui file di configurazione
Come descritto, logstash può essere avviato per un solo canale, in tal caso è necessario passare un file *.conf, oppure per più canali, nel qual caso deve essere passato un file pipelines.yml, che a sua volta farà riferimento ai file .conf per ogni canale.
Abbiamo scelto il secondo approccio. L'abbiamo trovato più universale e scalabile. Pertanto, abbiamo creato pipelines.yml e abbiamo fatto una directory pipelines, dove inseriremo i 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 directory:

Per ottenere i dati di input, consideriamo per ora che si tratti di tcp sulla porta 5046, e per l'output utilizzeremo stdout.
Ecco una semplice configurazione per il primo avvio. Del resto, l'obiettivo iniziale è avviarlo.
Quindi, abbiamo questo 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?
- Networks e volumes sono stati presi dal docker-compose.yml originale (quello in cui viene avviato l'intero stack) e non penso che influenzino molto l'immagine generale.
- Creiamo un servizio (services) logstash, dall'immagine docker.elastic.co/logstash/logstash:6.3.2 e gli diamo il nome logstash_one_channel.
- Stiamo inoltrando il porto 5046 all'interno del contenitore, sulla stessa porta interna.
- Stiamo montando 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 preleverà, rendendolo read-only, giusto per precauzione.
- Visualizziamo 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 identificativo 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 a eseguirlo:
docker-compose up
Cosa vediamo?
Il contenitore è partito. Possiamo controllarne il funzionamento:
echo '13123123123123123123123213123213' | nc localhost 5046
E vediamo in console la risposta del contenitore:

Ma vediamo anche:
logstash_one_channel | [2019-04-29T11:28:59,790][ERROR][logstash.licensechecker.licensereader] Impossibile recuperare le informazioni sulla licenza dal server delle licenze {:message=>«Elasticsearch non raggiungibile: [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. Si prega di 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] Controllo dello stato per vedere se la connessione a 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 non funzionante, 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] Controllo dello stato per vedere se la connessione a 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 non funzionante, 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 è partito con successo, in rosso — il messaggio di errore e in giallo — il messaggio del tentativo di contattare :9200.
Questo accade perché nel logstash.conf, incluso nell'immagine, c'è un controllo sulla disponibilità di elasticsearch. Infatti, logstash presuppone di funzionare all'interno del stack Elk, mentre noi lo abbiamo separato.
Si può lavorare, ma è scomodo.
La soluzione è disattivare questo controllo tramite la variabile d'ambiente XPACK_MONITORING_ENABLED.
Apportiamo la modifica a 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 esperimenti.
Possiamo digitare di nuovo nella console vicina:
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" => "Hello Habr",
logstash_one_channel | "host" => "gateway",
logstash_one_channel | "port" => 49418
logstash_one_channel | }
Lavorare all'interno di un solo canale
Ora che siamo partiti, possiamo dedicarci alla configurazione di logstash. Non toccheremo 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 vuoi leggere in italiano, abbiamo utilizzato questo (ma la sintassi delle query è obsoleta, bisogna tenerne conto).
Procediamo passo dopo passo dalla sezione Input. Abbiamo già visto il funzionamento tramite tcp. Cosa può esserci di interessante qui?
Messaggi di test, utilizzando heartbeat
C'è una possibilità interessante di generare messaggi di test automatici.
Per fare ciò, bisogna includere il plugin heartbeat nella sezione input.
input {
heartbeat {
message => "HeartBeat!"
}
}
Attiviamolo e iniziamo a ricevere
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.
In questo modo riceveremo un messaggio ogni 10 secondi.
input {
heartbeat {
message => "HeartBeat!"
interval => 10
}
}
Recupero dati dal file
Abbiamo anche deciso di esaminare la modalità file. Se funziona bene con il file, potrebbe non essere necessario alcun agente, almeno per l'uso locale.
Da quanto descritto, la modalità operativa dovrebbe essere simile a tail -f, cioè legge le nuove righe oppure, 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 però la possibilità di separare da dove proviene ogni dato.
- Vogliamo verificare che al riavvio di logstash non riceva nuovamente questi dati.
- Vogliamo verificare che se logstash è disattivato e i dati continuano a essere scritti nei file, quando lo riavviamo, quei dati vengano ricevuti.
Per condurre l'esperimento, aggiungeremo un'altra riga in docker-compose.yml, aprendo la directory in cui poniamo 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 modifichiamo la sezione input in habr_pipeline.conf
input {
file {
path => "/usr/share/logstash/input/*.log"
}
}
Partiamo:
docker-compose up
Per creare e registrare i log utilizzeremo il comando:
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 | }
Sì, funziona!
In questo modo, vediamo che è stato automaticamente aggiunto il campo path. Quindi, in seguito, potremo filtrare le registrazioni in base a questo campo.
Proviamo ancora:
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 | }
E ora in un altro file:
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 | }
Ottimo! Il file è stato catturato, il percorso è corretto, tutto bene.
Fermiamo logstash e lo riavviamo. Aspettiamo. Silenzio. Cioè, queste registrazioni non le riceviamo di nuovo.
E ora il esperimento più audace.
Mettiamo logstash e eseguiamo:
echo '3' >> logs/number2.log
echo '4' >> logs/number1.log
Riavviamo di nuovo logstash e vediamo:
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 | }
Evviva! Tutto è stato catturato.
Tuttavia, è necessario avvertire quanto segue. Se il contenitore con logstash viene rimosso (docker stop logstash_one_channel && docker rm logstash_one_channel), allora non verrà acquisito nulla. All'interno del contenitore è stata salvata la posizione del file fino a dove è stato letto. Se si avvia "da zero", accetterà solo nuove righe.
Lettura di file già esistenti
Supponiamo che stiamo avviando logstash per la prima volta, ma abbiamo già dei log che vorremmo elaborare.
Se avviamo logstash con la sezione input utilizzata sopra, non otterremo nulla. Solo le nuove righe saranno elaborate da logstash.
Per garantire che vengano estratte righe dai file esistenti, dobbiamo aggiungere una riga aggiuntiva alla sezione input:
input {
file {
start_position => "beginning"
path => "/usr/share/logstash/input/*.log"
}
}
Va notato che questo funziona solo per nuovi file che logstash non ha ancora visto. Per i file che erano già stati visti da logstash, ha già memorizzato la loro dimensione e ora acquisirà solo le nuove voci in essi.
Fermiamoci qui nello studio della sezione input. Ci sono molte altre opzioni, ma per i nostri esperimenti futuri, questo è sufficiente per ora.
Instradamento e trasformazione dei dati
Proviamo a risolvere il seguente compito: supponiamo che ci siano messaggi provenienti da un canale, alcuni dei quali sono informativi e altri riguardano errori. Si differenziano per un 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 questo, passiamo dalla sezione input a filter e output.
Con la sezione filter analizzeremo il messaggio in ingresso, ottenendo un hash (coppie chiave-valore) su cui possiamo lavorare, ovvero analizzarlo in base a determinate condizioni. Nella sezione output, selezioneremo i messaggi e invieremo ognuno nel proprio canale.
Analisi del messaggio con grok
Per analizzare le stringhe testuali e ottenere un insieme di campi, nella sezione filter c'è un plugin speciale: grok.
Senza l'intento di fornire qui una descrizione dettagliata (per questo rimando a ), presenterò un semplice esempio.
Per questo, è necessario definire il formato delle stringhe in ingresso. Le mie sono così:
1 INFO message1
2 ERROR message2
Cioè, l'identificatore è al primo posto, seguito da INFO/ERROR e poi da una parola senza spazi.
Non è complicato, ma basta per comprendere il principio di funzionamento.
Quindi, nella sezione filter del plugin grok, dobbiamo definire un modello per analizzare le nostre righe.
Sarà così:
filter {
grok {
match => { "message" => ["%{INT:message_id} %{LOGLEVEL:message_type} %{WORD:message_text}"] }
}
}
In sostanza, questa è un'espressione regolare. Si usano modelli già pronti, come INT, LOGLEVEL e WORD. La loro descrizione, così come altri modelli, può essere consultata qui
Ora, passando attraverso questo filtro, la nostra riga si trasformerà in un hash con tre campi: message_id, message_type, message_text.
Questi saranno quelli 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. Quelli che sono INFO verranno visualizzati sulla console, mentre gli errori verranno scritti su un file.
Come possiamo separare questi messaggi? La condizione del compito ci suggerisce già la soluzione: abbiamo infatti già un campo message_type che può assumere solo due valori, INFO ed ERROR. Proprio su di esso faremo la scelta utilizzando l'operatore if.
if [message_type] == "ERROR" {
# Qui scriviamo nel file
} else
{
# Qui scriviamo in stdout
}
La descrizione del lavoro con campi e operatori può essere consultata qui nella sezione .
Passiamo ora all'output stesso.
L'output sulla console è chiaro: stdout {}
Tuttavia, riguardo all'output su file, ricordiamo che stiamo eseguendo tutto questo da un container e affinché il file in cui scriviamo il risultato 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 {
}
}
}
Aggiungiamo un altro volume in docker-compose.yml 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 e vediamo la separazione in due flussi.
Fonte: habr.com
