Raccogliamo i log con Loki

Raccogliamo i log con Loki

In Badoo monitoriamo costantemente le nuove tecnologie e valutiamo se sia il caso di utilizzarle nel nostro sistema. Vogliamo condividere con la comunità una di queste ricerche. Essa è dedicata a Loki — un sistema di aggregazione dei log.

Loki è una soluzione per l'archiviazione e la visualizzazione dei log; questo stack fornisce anche un sistema flessibile per la loro analisi e per inviare dati a Prometheus. A maggio è uscito un nuovo aggiornamento, che i creatori stanno promuovendo attivamente. Siamo interessati a scoprire le funzionalità di Loki, quali opportunità offre e in quale misura può fungere da alternativa all'ELK — lo stack che stiamo utilizzando attualmente.

Cos'è Loki

Grafana Loki è un insieme di componenti per un sistema completo di gestione dei log. A differenza di altri sistemi simili, Loki si basa sull'idea di indicizzare solo i metadati dei log — labels (proprio come in Prometheus), mentre i log stessi vengono compressi in chunk separati.

Pagina principale, GitHub

Prima di passare alla descrizione di ciò che si può fare con Loki, voglio chiarire cosa si intende per "idea di indicizzare solo i metadati". Confrontiamo l'approccio di Loki e l'approccio all'indicizzazione nelle soluzioni tradizionali, come Elasticsearch, utilizzando una riga di log di nginx come esempio:

172.19.0.4 - - [01/Giu/2020:12:05:03 +0000] "GET /purchase?user_id=75146478&item_id=34234 HTTP/1.1" 500 8102 "-" "Stub_Bot/3.0" "0.001"

I sistemi tradizionali analizzano l'intera stringa, comprese le aree con un gran numero di valori unici come user_id e item_id, e memorizzano tutto in grandi indici. Il vantaggio di questo approccio è che è possibile eseguire query complesse rapidamente, poiché quasi tutti i dati si trovano nell'indice. Tuttavia, questo comporta il costo di un indice enorme, che si traduce in requisiti di memoria elevati. Di fatto, l'indice full-text dei log è comparabile in dimensione agli stessi log. Per cercare rapidamente attraverso di esso, l'indice deve essere caricato in memoria. E più log ci sono, più rapidamente l'indice cresce e maggiore è la memoria che consuma.

L'approccio di Loki richiede l'estrazione solo dei dati necessari da una stringa, il cui numero è relativamente limitato. In questo modo, otteniamo un indice ridotto e possiamo cercare i dati filtrandoli per tempo e per campi indicizzati, per poi scansionare il resto con espressioni regolari o per ricerca di sottostringa. Il processo può sembrare non molto veloce, ma Loki suddivide la query in più parti ed esegue queste operazioni in parallelo, elaborando una grande quantità di dati in breve tempo. Il numero di shard e di richieste parallele in essi può essere configurato; così, la quantità di dati che possono essere elaborati per unità di tempo dipende linearmente dalle risorse fornite.

Questo compromesso tra un grande indice veloce e un indice più piccolo con una ricerca completa parallela consente a Loki di controllare i costi del sistema. Può essere facilmente configurato e scalato in base alle necessità.

Il stack Loki è composto da tre componenti: Promtail, Loki e Grafana. Promtail raccoglie i log, li elabora e li invia a Loki. Loki li memorizza. Grafana permette invece di interrogare i dati da Loki e di visualizzarli. In effetti, Loki può essere utilizzato non solo per memorizzare i log e cercare in essi. L'intero stack offre ampie possibilità per l'elaborazione e l'analisi dei dati in arrivo, utilizzando il metodo Prometheus.
La descrizione del processo di installazione può essere trovata qui.

Ricerca nei log

La ricerca nei log può essere fatta attraverso l'interfaccia speciale di Grafana — Explorer. Per le interrogazioni, si utilizza il linguaggio LogQL, molto simile al PromQL, usato in Prometheus. In un certo senso, può essere considerato come un grep distribuito.

L'interfaccia di ricerca appare così:

Raccogliamo i log con Loki

L'interrogazione stessa è composta da due parti: selector e filter. Il selector è la ricerca nei metadati indicizzati (etichette) assegnati ai log, mentre il filter è una stringa di ricerca o una regex attraverso la quale vengono filtrati i record definiti dal selector. Nell'esempio fornito: Le parentesi graffe denotano il selector, tutto ciò che segue è il filtro.

{image_name="nginx.promtail.test"} |= "index"

A causa del principio di funzionamento di Loki, non è possibile eseguire interrogazioni senza il selector, ma le etichette possono essere rese quanto più generali possibile.

Il selettore è una coppia chiave-valore racchiusa tra parentesi graffe. È possibile combinare i selettori e impostare diverse condizioni di ricerca utilizzando gli operatori =, != o le espressioni regolari:

{instance=~"kafka-[23]",name!="kafka-dev"} 
// Troverà i log con l'etichetta instance, aventi valore kafka-2, kafka-3, ed escluderà dev 

Il filtro è un testo o un regex che filtrerà tutte le informazioni ottenute dal selettore.

È possibile ottenere grafici ad-hoc dai dati ricevuti in modalità metrics. Ad esempio, si può scoprire la frequenza di apparizione nei log di nginx delle voci contenenti la stringa index:

Raccogliamo i log con Loki

Una descrizione completa delle funzionalità è disponibile nella documentazione LogQL.

Parsing dei log

Ci sono diversi modi per raccogliere i log:

  • Utilizzando Promtail, il componente standard del framework per la raccolta dei log.
  • Direttamente dal container Docker tramite il driver di logging Docker di Loki.
  • Utilizzare Fluentd o Fluent Bit, che possono inviare dati a Loki. A differenza di Promtail, possiedono parser già pronti per quasi ogni tipo di log e gestiscono anche i log multiline.

Di solito, si utilizza Promtail per il parsing. Esso esegue tre attività:

  • Trova le fonti di dati.
  • Aggiunge etichette ad esse.
  • Invia i dati a Loki.

Attualmente Promtail può leggere i log da file locali e dal journal di systemd. Deve essere installato su ogni macchina da cui si raccolgono i log.

Esiste un'integrazione con Kubernetes: Promtail rileva automaticamente lo stato del cluster tramite l'API REST di Kubernetes e raccoglie i log da nodo, servizio o pod, aggiungendo immediatamente le etichette basate sui metadati di Kubernetes (nome del pod, nome file, ecc.).

È possibile anche aggiungere etichette basate sui dati dai log tramite un Pipeline. Il Pipeline di Promtail può consistere in quattro tipi di fasi. Maggiori dettagli - documentazione ufficiale, qui segnalerò alcune particolarità.

  1. Fasi di Parsing. Questa fase riguarda RegEx e JSON. In questo passaggio estraiamo i dati dai log nella cosiddetta extracted map. I dati possono essere estratti da JSON semplicemente copiando i campi necessari nella extracted map, oppure tramite espressioni regolari (RegEx), dove nella extracted map si ‘mappano’ i named groups. L'extracted map è un archivio key-value, dove la chiave è il nome del campo e il valore è il suo contenuto dai log.
  2. Fasi di Trasformazione. Questo stadio ha due opzioni: transform, dove definiamo le regole di trasformazione, e source — la fonte dei dati per la trasformazione dall'extracted map. Se tale campo non è presente nell'extracted map, verrà creato. In questo modo è possibile creare etichette che non si basano sull'extracted map. In questo passaggio possiamo manipolare i dati nell'extracted map utilizzando strumenti piuttosto potenti. Golang Template. Inoltre, è importante ricordare che l'extracted map viene completamente caricata durante il parsing, il che consente, ad esempio, di verificare un valore in essa: “{{if .tag}il valore del tag esiste{end}}”. Il template supporta condizioni, cicli e alcune funzioni stringa, come Replace e Trim.
  3. Action stages. In questo stadio, è possibile eseguire operazioni sui dati estratti:
    • Creare un'etichetta dai dati estratti, che verrà indicizzata da Loki.
    • Modificare o impostare il timestamp dell'evento dal log.
    • Modificare i dati (testo del log) che verranno inviati a Loki.
    • Creare metriche.
  4. Filtering stages. Stadio match, in cui puoi inviare a /dev/null i record che non ci servono o indirizzarli per ulteriori elaborazioni.

Mostrerò con un esempio di trattamento di normali log nginx, come è possibile analizzare i log utilizzando Promtail.

Per il test utilizzeremo un'immagine modificata di nginx come nginx-proxy jwilder/nginx-proxy:alpine e un piccolo demone che può interrogare se stesso tramite HTTP. Il demone ha diversi endpoint a cui può rispondere con dimensioni di risposta variabili, stati HTTP diversi e latenze differenti.

Raccoglieremo i log dai contenitori Docker, che si possono trovare nel percorso /var/lib/docker/containers//-json.log

In docker-compose.yml configuriamo Promtail e indichiamo il percorso del file di configurazione:

promtail:
  image: grafana/promtail:1.4.1
 // ...
 volumes:
   - /var/lib/docker/containers:/var/lib/docker/containers:ro
   - promtail-data:/var/lib/promtail/positions
   - ${PWD}/promtail/docker.yml:/etc/promtail/promtail.yml
 command:
   - '-config.file=/etc/promtail/promtail.yml'
 // ...

Aggiungiamo a promtail.yml il percorso dei log (nel file di configurazione c'è un'opzione 'docker' che fa la stessa cosa in una riga, ma non sarebbe così chiaro):

scrape_configs:
 - job_name: containers

   static_configs:
       labels:
         job: containerlogs
         __path__: /var/lib/docker/containers/*/*log  # solo per linux

Abilitando questa configurazione, i log di tutti i contenitori verranno inviati a Loki. Per evitare ciò, modifichiamo le impostazioni del test nginx in docker-compose.yml — aggiungiamo la registrazione con il campo tag:

proxy:
 image: nginx.test.v3
//…
 logging:
   driver: "json-file"
   options:
     tag: "{{.ImageName}}|{{.Name}}"

Modifichiamo promtail.yml e configuriamo il Pipeline. I log in entrata sono del seguente tipo:

{"log":"u001b[0;33;1mnginx.1    | u001b[0mnginx.test 172.28.0.3 - - [13/Giu/2020:23:25:50 +0000] \"GET /api/index HTTP/1.1\" 200 0 \"-\" \"Stub_Bot/0.1\" \"0.096\"n","stream":"stdout","attrs":{"tag":"nginx.promtail.test|proxy.prober"},"time":"2020-06-13T23:25:50.66740443Z"}
{"log":"u001b[0;33;1mnginx.1    | u001b[0mnginx.test 172.28.0.3 - - [13/Giu/2020:23:25:50 +0000] \"GET /200 HTTP/1.1\" 200 0 \"-\" \"Stub_Bot/0.1\" \"0.000\"n","stream":"stdout","attrs":{"tag":"nginx.promtail.test|proxy.prober"},"time":"2020-06-13T23:25:50.702925272Z"}

Fase del pipeline:

 - json:
     espressioni:
       stream: stream
       attrs: attrs
       tag: attrs.tag

Estraiamo dai JSON in ingresso i campi stream, attrs, attrs.tag (se presenti) e li inseriamo nella mappa estratta.

 - regex:
     espressione: ^(?P([^|]+))|(?P([^|]+))$
     sorgente: "tag"

Se il campo tag è stato correttamente inserito nella mappa estratta, utilizziamo l'expression per estrarre i nomi dell'immagine e del contenitore.

 - labels:
     image_name:
     container_name:

Assegniamo le etichette. Se nella data estratta vengono trovate le chiavi image_name e container_name, i loro valori saranno assegnati alle etichette corrispondenti.

 - match:
     selettore: '{job="docker",container_name="",image_name=""}'
     azione: scarta

Scartiamo tutti i log che non hanno le etichette image_name e container_name impostate.

  - match:
     selettore: '{image_name="nginx.promtail.test"}'
     fasi:
       - json:
           espressioni:
             row: log

Per tutti i log in cui image_name è uguale a nginx.promtail.test, estraiamo dal log originale il campo log e lo inseriamo nella mappa estratta con la chiave row.

  - regex:
         # sopprimi i colori forego
         expression: .+nginx.+|.+[0m(?P[a-z_.-]+) +(?P.+)
         source: logrow

Pulisci la stringa di ingresso con espressioni regolari ed estrai il virtual host nginx e la riga del log nginx.

     - regex:
         source: nginxlog
         expression: ^(?P[w.]+) - (?P[^ ]*) [(?P[^ ]+).*] "(?P[^ ]*) (?P[^ ]*) (?P[^ ]*)" (?P[d]+) (?P[d]+) "(?P[^"]*)" "(?P[^"]*)"( "(?P[d.]+)")?

Parser il log nginx con espressioni regolari.

    - regex:
           source: request_url
           expression: ^.+.(?Pjpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$
     - regex:
           source: request_url
           expression: ^/photo/(?P[^/?.]+).*$
       - regex:
           source: request_url
           expression: ^/api/(?P[^/?.]+).*$

Analizziamo request_url. Utilizzando regex, determiniamo lo scopo della richiesta: verso la risorsa statica, verso le foto, verso l'API e impostiamo nella mappa estratta la chiave corrispondente.

       - template:
           source: request_type
           template: "{{if .photo}}photo{{else if .static_type}}static{{else if .api_request}}api{{else}}other{{end}}"

Utilizzando gli operatori condizionali in Template, controlliamo i campi impostati nella mappa estratta e impostiamo i valori richiesti per il campo request_type: photo, static, API. Assegniamo other se non ci riesce. Ora request_type contiene il tipo di richiesta.

       - labels:
           api_request:
           virtual_host:
           request_type:
           status:

Impostiamo le etichette api_request, virtual_host, request_type e status (HTTP status) in base a ciò che siamo riusciti a inserire nella mappa estratta.

       - output:
           source: nginx_log_row

Modifichiamo l'output. Ora nel Loki viene inviato il log nginx pulito dalla mappa estratta.

Raccogliamo i log con Loki

Dopo aver avviato la configurazione fornita, è possibile vedere che a ciascun record sono state assegnate etichette in base ai dati del log.

È importante tenere presente che l'estrazione di etichette con un numero elevato di valori (cardinalità) può rallentare significativamente il funzionamento di Loki. Quindi non è consigliabile inserire nell'indice, ad esempio, user_id. Maggiori dettagli in "How labels in Loki can make log queries faster and easier". Tuttavia, questo non significa che non si possa cercare per user_id senza indici. È necessario utilizzare filtri durante la ricerca ("greppare" nei dati), mentre l'indice funge da identificatore del flusso.

Visualizzazione dei log

Raccogliamo i log con Loki

Loki può fungere da fonte di dati per i grafici Grafana, utilizzando LogQL. Sono supportate le seguenti funzioni:

  • rate — il numero di registrazioni al secondo;
  • count over time — il numero di registrazioni in un intervallo specificato.

Sono inoltre disponibili funzioni di aggregazione come Sum, Avg e altre. È possibile creare grafici piuttosto complessi, ad esempio un grafico sul numero di errori HTTP:

Raccogliamo i log con Loki

La fonte dati standard Loki ha funzionalità ridotte rispetto alla fonte dati Prometheus (ad esempio, non è possibile modificare la legenda), ma Loki può essere connesso come fonte di tipo Prometheus. Non sono sicuro che questo comportamento sia documentato, ma, basandomi sulla risposta degli sviluppatori “How to configure Loki as Prometheus datasource? · Issue #1222 · grafana/loki”, ad esempio, ciò è perfettamente legittimo e Loki è completamente compatibile con PromQL.

Aggiungiamo Loki come fonte dati di tipo Prometheus e aggiungiamo l'URL /loki:

Raccogliamo i log con Loki

E possiamo creare grafici, proprio come se stessimo lavorando con le metriche di Prometheus:

Raccogliamo i log con Loki

Penso che la disuguaglianza nelle funzionalità sia temporanea e gli sviluppatori la correggeranno in futuro.

Raccogliamo i log con Loki

Metriche

In Loki è possibile estrarre metriche numeriche dai log e inviarle a Prometheus. Ad esempio, nei log di nginx è presente il numero di byte per risposta e, con una modifica specifica del formato standard del log, anche il tempo in secondi impiegato per la risposta. Questi dati possono essere estratti e inviati a Prometheus.

Aggiungiamo un'altra sezione a promtail.yml:

- match:
   selector: '{request_type="api"}'
   stages:
     - metrics:
         http_nginx_response_time:
           type: Histogram
           description: "tempo di risposta ms"
           source: response_time
           config:
             buckets: [0.010, 0.050, 0.100, 0.200, 0.500, 1.0]
- match:
   selector: '{request_type=~"static|photo"}'
   stages:
     - metrics:
         http_nginx_response_bytes_sum:
           type: Counter
           description: "somma dei byte della risposta"
           source: bytes_out
           config:
             action: add
         http_nginx_response_bytes_count:
           type: Counter
           description: "conteggio dei byte della risposta"
           source: bytes_out
           config:
             action: inc

L'opzione consente di definire e aggiornare le metriche basate sui dati estratti dalla mappa. Queste metriche non vengono inviate a Loki, ma appaiono nell'endpoint Promtail /metrics. Prometheus deve essere configurato per ricevere i dati ottenuti in questa fase. Nell'esempio fornito, per request_type="api", raccogliamo una metrica a istogramma. Con questo tipo di metriche è facile ottenere percentili. Per la statica e le foto, raccogliamo la somma dei byte e il numero di righe in cui abbiamo ottenuto byte, per calcolare il valore medio.

Leggi di più sulle metriche qui.

Apriamo la porta su Promtail:

promtail:
     image: grafana/promtail:1.4.1
     container_name: monitoring.promtail
     expose:
       - 9080
     ports:
       - "9080:9080"

Assicuriamoci che le metriche con il prefisso promtail_custom siano apparse:

Raccogliamo i log con Loki

Configuriamo Prometheus. Aggiungiamo il job promtail:

- job_name: 'promtail'
 scrape_interval: 10s
 static_configs:
   - targets: ['promtail:9080']

E tracciamo il grafico:

Raccogliamo i log con Loki

In questo modo, ad esempio, è possibile scoprire le quattro richieste più lente. È anche possibile impostare il monitoraggio su queste metriche.

Scalabilità

Loki può operare sia in modalità singola (single binary mode) che in modalità shardabile (horizontally-scalable mode). Nel secondo caso, può archiviare i dati nel cloud, con i chunk e l'indice memorizzati separatamente. Nella versione 1.5 è stata implementata la possibilità di archiviare tutto in un'unica posizione, ma attualmente non è consigliato utilizzarla in produzione.

Raccogliamo i log con Loki

I chunk possono essere memorizzati in uno storage compatibile con S3, mentre per gli indici è consigliabile utilizzare database orizzontalmente scalabili come Cassandra, BigTable o DynamoDB. Altre parti di Loki — Distributors (per la scrittura) e Querier (per le query) — sono senza stato e possono essere scalati orizzontalmente.

Alla conferenza DevOpsDays Vancouver 2019, uno dei partecipanti, Callum Styan, ha dichiarato che con Loki il suo progetto gestisce petabyte di log con un indice inferiore all'1% delle dimensioni totali: “Come Loki Correlates Metrics and Logs — E Ti Fa Risparmiare Denaro”.

Confronto tra Loki e ELK

Dimensione dell'indice

Per testare la dimensione dell'indice ottenuta, ho utilizzato i log da un container nginx, per il quale era stato configurato il Pipeline descritto sopra. Il file di log conteneva 406.624 righe per un volume totale di 109 MB. I log sono stati generati per un'ora, circa 100 registrazioni al secondo.

Esempio di due righe dal log:

Raccogliamo i log con Loki

Durante l'indicizzazione con ELK, questo ha prodotto una dimensione dell'indice di 30,3 MB:

Raccogliamo i log con Loki

Nel caso di Loki, questo ha fornito circa 128 Kb di indice e circa 3,8 Mb di dati in chunk. È importante notare che il log è stato generato artificialmente e non presentava una grande varietà di dati. Un semplice gzip sul log JSON originale di Docker con i dati ha fornito una compressione del 95,4%, e considerando che nel Loki veniva inviato solo il log nginx ripulito, è comprensibile la compressione a 4 Mb. Il numero totale di valori unici per le etichette di Loki era 35, il che spiega la piccola dimensione dell'indice. Anche per ELK, il log è stato ripulito. In questo modo, Loki ha compresso i dati originali del 96%, mentre ELK del 70%.

Consumo di memoria

Raccogliamo i log con Loki

Confrontando l'intero stack Prometheus ed ELK, Loki consuma significativamente meno. È chiaro che un servizio in Go consuma meno rispetto a uno in Java, e il confronto tra la dimensione dell'heap JVM di Elasticsearch e la memoria allocata per Loki non è corretto, ma si deve comunque notare che Loki utilizza molta meno memoria. Il suo vantaggio in termini di CPU non è così evidente, ma è comunque presente.

Velocità

Loki è più rapido nel "catturare" i log. La velocità dipende da molti fattori: che tipo di log sono, quanto complessamente li analizziamo, la rete, il disco, ecc. Tuttavia, è sicuramente superiore rispetto a ELK (nel mio test, circa il doppio). Questo è spiegato dal fatto che Loki inserisce molte meno informazioni nell'indice, e di conseguenza, impiega meno tempo per indicizzare. Tuttavia, la situazione è inversa per quanto riguarda la velocità di ricerca: Loki rallenta sensibilmente con dati superiori a pochi gigabyte, mentre per ELK la velocità di ricerca non dipende dalle dimensioni dei dati.

Ricerca nei log

Loki è significativamente inferiore a ELK per quanto riguarda le capacità di ricerca nei log. Grep con espressioni regolari è uno strumento potente, ma non si può paragonare a un database maturo. La mancanza di query per range, l'aggregazione solo per etichette e l'impossibilità di cercare senza etichette limitano la nostra possibilità di trovare informazioni rilevanti in Loki. Ciò non significa che non si possa trovare nulla con Loki, ma definisce il flusso di lavoro con i log, dove prima si identifica un problema attraverso i grafici di Prometheus e poi, utilizzando queste etichette, si cerca cosa sia accaduto nei log.

Interfaccia

In primo luogo, è bello (scusate, non ho potuto resistere). Grafana ha un'interfaccia gradevole, ma Kibana è molto più funzionale.

Pro e contro di Loki

Tra i punti a favore, si può notare che Loki si integra con Prometheus, quindi otteniamo metriche e allerta direttamente. È comodo per raccogliere e archiviare i log insieme ai Pods di Kubernetes, poiché eredita dal service discovery di Prometheus e aggiunge automaticamente le etichette.

Tra i punti a sfavore, la documentazione è debole. Alcune cose, come le peculiarità e le funzionalità di Promtail, le ho scoperte solo studiando il codice, per fortuna open-source. Un altro difetto sono le limitate capacità di parsing. Ad esempio, Loki non può analizzare log multilinea. Inoltre, tra i difetti c'è il fatto che Loki è una tecnologia relativamente nuova (il rilascio 1.0 è avvenuto a novembre 2019).

Conclusione

Loki è una tecnologia al 100% interessante, adatta per progetti piccoli e medi, permettendo di affrontare molteplici compiti di aggregazione dei log, ricerca tra i log, monitoraggio e analisi dei log.

Non utilizziamo Loki in Badoo, poiché abbiamo un stack ELK che ci soddisfa e nel quale, nel corso degli anni, abbiamo sviluppato diverse soluzioni personalizzate. Per noi, la sfida principale è la ricerca nei log. Con quasi 100 GB di log al giorno, è fondamentale poter trovare tutto e anche di più, e farlo rapidamente. Per la creazione di grafici e il monitoraggio utilizziamo altre soluzioni, progettate per le nostre esigenze e integrate tra loro. L'stack Loki ha vantaggi significativi, ma non ci fornirà di più rispetto a ciò che già abbiamo, e i suoi benefici non giustificherebbero il costo della migrazione.

E anche se dopo la ricerca è diventato chiaro che non possiamo utilizzare Loki, speriamo che questo post possa aiutarvi nella scelta.

Il repository con il codice utilizzato nell'articolo si trova qui.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster