Raccogliamo i log con Loki

Raccogliamo i log con Loki

In Badoo monitoriamo costantemente le nuove tecnologie e valutiamo se utilizzarle nel nostro sistema. Vogliamo condividere con la comunità uno di questi studi. È dedicato a Loki, un sistema di aggregazione dei log.

Loki è una soluzione per la memorizzazione e la visualizzazione dei log, e questo stack offre un sistema flessibile per la loro analisi e l'invio dei dati a Prometheus. A maggio è uscito un altro aggiornamento, che i creatori stanno promuovendo attivamente. Ci ha incuriosito sapere cosa può fare Loki, quali opportunità offre e in che misura può fungere da alternativa a ELK, lo stack che utilizziamo 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 — le etichette (come in Prometheus), mentre i log stessi vengono compressi in chunk separati.

Pagina principale, GitHub

Prima di passare alla descrizione di ciò che è possibile fare con Loki, voglio spiegare 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 un esempio di una riga di log di nginx:

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 eseguono il parsing dell'intera stringa, inclusi campi con un gran numero di valori unici come user_id e item_id, e salvano tutto in grandi indici. Il vantaggio di questo approccio è che è possibile eseguire query complesse rapidamente, poiché quasi tutti i dati sono nell'indice. Tuttavia, ciò comporta un costo in termini di dimensioni dell'indice, il che porta a requisiti di memoria elevati. Alla fine, un indice di testo completo dei log è comparabile in dimensione ai log stessi. Per cercare rapidamente, l'indice deve essere caricato in memoria. Più log ci sono, più velocemente l'indice cresce e maggiore è la memoria che consuma.

L'approccio di Loki richiede che dalla stringa vengano estratti solo i dati necessari, la cui quantità è limitata. In questo modo, otteniamo un piccolo indice e possiamo cercare i dati, filtrandoli per tempo e per campi indicizzati, e poi scandire il restante con espressioni regolari o cercando sottostringhe. Il processo sembra non essere il più veloce, ma Loki divide la richiesta in più parti ed esegue in parallelo, elaborando un grande volume di dati in breve tempo. Il numero di shard e delle richieste parallele in essi è configurabile; pertanto, la quantità di dati che possono essere elaborati in un'unità di tempo dipende linearmente dalla quantità di risorse fornite.

Questo compromesso tra un grande indice veloce e un piccolo indice con scansione completa parallela permette a Loki di controllare i costi del sistema. Può essere configurato e scalato in modo flessibile in base alle necessità.

Il stack di Loki è composto da tre componenti: Promtail, Loki, Grafana. Promtail raccoglie i log, li elabora e li invia a Loki. Loki li memorizza. Grafana è in grado di interrogare i dati di Loki e di mostrarli. In effetti, Loki può essere utilizzato non solo per memorizzare i log e cercarli. L'intero stack offre ampie possibilità di elaborazione e analisi dei dati in entrata, utilizzando il Prometheus way.
Una descrizione del processo di installazione può essere trovata qui.

Ricerca nei log

La ricerca nei log può avvenire in un'interfaccia speciale di Grafana — Explorer. Per le richieste si utilizza il linguaggio LogQL, molto simile a PromQL, utilizzato in Prometheus. In linea di principio, può essere considerato come un grep distribuito.

L'interfaccia di ricerca appare così:

Raccogliamo i log con Loki

La richiesta consiste in due parti: selector e filter. Il selector è la ricerca nei metadati indicizzati (etichette) assegnati ai log, mentre il filter è la stringa di ricerca o l'espressione regolare con cui vengono filtrati i record definiti dal selector. Nell'esempio fornito: Tra parentesi graffe troviamo il selector, tutto ciò che segue è il filter.

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

A causa del principio di funzionamento di Loki, non è possibile effettuare richieste senza selector, ma le etichette possono essere rese il più generiche possibile.

Il selector è costituito da coppie chiave-valore racchiuse tra parentesi graffe. È possibile combinare i selector e definire diverse condizioni di ricerca utilizzando operatori =, != o espressioni regolari:

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

Un filtro è un testo o un'espressione regolare che filtra tutti i dati ottenuti dal selettore.

È possibile ottenere grafici ad-hoc dai dati ricevuti in modalità metrics. Ad esempio, è possibile scoprire la frequenza con cui appare nei log di nginx una registrazione contenente 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 della stack per la raccolta dei log.
  • Direttamente dal container Docker usando Il driver di logging Docker Loki.
  • Utilizzando Fluentd o Fluent Bit, che sono in grado di inviare dati a Loki. A differenza di Promtail, hanno parser pronti praticamente per qualsiasi tipo di log e riescono anche a gestire log multilinea.

Di solito, per il parsing si utilizza Promtail. Esso svolge tre compiti:

  • Trova le fonti dei dati.
  • Aggiunge loro delle etichette.
  • 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 vengono raccolti i log.

C'è un'integrazione con Kubernetes: Promtail scopre automaticamente lo stato del cluster tramite l'API REST di Kubernetes e raccoglie i log da nodo, servizio o pod, assegnando immediatamente le etichette in base ai metadati di Kubernetes (nome del pod, nome del file, ecc.).

È inoltre possibile assegnare etichette in base ai dati del log tramite il Pipeline. Un Pipeline di Promtail può consistere in quattro tipi di fasi. Maggiori dettagli sono disponibili nel documentazione ufficiale, qui sottolineo alcune sfumature.

  1. Fasi di parsing. Questa fase riguarda RegEx e JSON. In questa fase estraiamo dati dai log in quella che viene chiamata extracted map. È possibile estrarre da JSON semplicemente copiando i campi necessari nell'extracted map, oppure tramite espressioni regolari (RegEx), dove nell'extracted map vengono "mappati" i named groups. L'extracted map è una memorizzazione key-value, dove key è il nome del campo e value è il suo valore dai log.
  2. Fasi di trasformazione. Questa fase ha due opzioni: transform, dove definiamo le regole di trasformazione, e source — la fonte dei dati per la trasformazione dall'extracted map. Se nell'extracted map non esiste tale campo, verrà creato. In questo modo, è possibile creare etichette non basate sull'extracted map. A questo punto, possiamo manipolare i dati nell'extracted map, utilizzando un Golang Template. Inoltre, è importante ricordare che la mappa estratta viene completamente caricata durante il parsing, il che consente ad esempio di verificare un valore al suo interno: “{{if .tag}il valore del tag esiste{end}}”. Il template supporta condizioni, cicli e alcune funzioni stringa, come Replace e Trim.
  3. Fasi dell'azione. In questa fase è possibile fare qualcosa con l'estratto:
    • Creare un'etichetta dai dati estratti, che verrà indicizzata da Loki.
    • Modificare o impostare il tempo dell'evento dal log.
    • Modificare i dati (testo del log) che andranno a Loki.
    • Creare metriche.
  4. Fasi di filtraggio. Fase di match, in cui è possibile inviare in /dev/null le registrazioni di cui non abbiamo bisogno, oppure indirizzarle per ulteriori elaborazioni.

Mostrerò un esempio di come elaborare i log nginx standard utilizzando Promtail.

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

Raccoglieremo i log dai contenitori Docker, che possono essere trovati nel percorso /var/lib/docker/containers/<container_id>/<container_id>-json.log

Nel 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 in promtail.yml il percorso dei log (nel config è presente l'opzione "docker", che fa la stessa cosa in una sola riga, ma questo sarebbe meno chiaro):

scrape_configs:
 - job_name: containers

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

Attivando questa configurazione, i log di tutti i contenitori verranno inviati a Loki. Per evitarlo, modifichiamo le impostazioni del nginx di test nel docker-compose.yml — aggiungiamo il logging con il campo tag:

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

Modifichiamo promtail.yml e configuriamo il Pipeline. In ingresso arrivano log di questo 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:
     expressions:
       stream: stream
       attrs: attrs
       tag: attrs.tag

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

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

Se siamo riusciti a inserire il campo tag nella mappa estratta, utilizziamo una regex per estrarre i nomi dell'immagine e del contenitore.

 - etichette:
     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 rispettive etichette.

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

Scartiamo tutti i log nei quali non sono state trovate le etichette installate image_name e container_name.

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

Per tutti i log nei quali image_name è uguale a nginx.promtail.test, estraiamo dal log originale il campo log e lo mettiamo nella mappa estratta con la chiave riga.

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

Puliamo la stringa di input con espressioni regolari ed estraiamo l'host virtuale nginx e la riga del log nginx.

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

Analizziamo il log nginx con espressioni regolari.

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

Analizziamo request_url. Con l'aiuto della regex, determiniamo la destinazione della richiesta: statico, foto, API e impostiamo la chiave corrispondente nella mappa estratta.

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

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

       - etichette:
           api_request:
           virtual_host:
           request_type:
           status:

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

       - output:
           sorgente: nginx_log_row

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

Raccogliamo i log con Loki

Dopo aver avviato la configurazione fornita, possiamo vedere che a ciascun record sono state assegnate etichette basate sui dati del log.

È necessario tenere presente che l'estrazione di etichette con un alto numero di valori (cardinalità) può rallentare significativamente il funzionamento di Loki. Quindi, non è opportuno inserire, ad esempio, user_id nell'indice. Maggiori dettagli su questo possono essere trovati nell'articolo “Come le etichette in Loki possono rendere le query di log più veloci e facili”. Ma questo non significa che non si possa cercare per user_id senza indici. È necessario utilizzare filtri durante la ricerca (eseguire un grep sui dati), mentre l'indice funge da identificatore di flusso.

Visualizzazione dei log

Raccogliamo i log con Loki

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

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

Sono inoltre disponibili funzioni aggreganti come Sum, Avg e altre. È possibile costruire grafici abbastanza complessi, ad esempio un grafico del numero di errori HTTP:

Raccogliamo i log con Loki

La fonte di dati standard Loki ha alcune funzionalità ridotte rispetto alla fonte di dati Prometheus (ad esempio, non è possibile modificare la legenda), ma Loki può essere collegato come fonte di tipo Prometheus. Non sono sicuro che questo comportamento sia documentato, ma, a giudicare dalla risposta degli sviluppatori “Come configurare Loki come fonte di dati Prometheus? · Issue #1222 · grafana/loki”, per esempio, questo è del tutto legittimo e Loki è completamente compatibile con PromQL.

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

Raccogliamo i log con Loki

E si possono realizzare grafici, come nel caso in cui lavorassimo con metriche da Prometheus:

Raccogliamo i log con Loki

Penso che la divergenza nelle funzionalità sia temporanea e che gli sviluppatori la sistemeranno in futuro.

Raccogliamo i log con Loki

Metriche

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

Aggiungiamo un'altra sezione in 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 di risposta"
           source: bytes_out
           config:
             action: add
         http_nginx_response_bytes_count:
           type: Counter
           description: "conteggio dei byte di risposta"
           source: bytes_out
           config:
             action: inc

Questa opzione consente di definire e aggiornare le metriche basate sui dati estratti dalla mappa. Queste metriche non vengono inviate a Loki — appaiono nell'endpoint Promtail /metrics. Prometheus deve essere configurato in modo da ricevere i dati generati in questa fase. Nell'esempio fornito, per request_type="api", raccogliamo una metrica a istogramma. Con questo tipo di metriche è comodo ottenere percentile. Per le statistiche e le foto, raccogliamo la somma dei byte e il numero di righe in cui abbiamo ricevuto byte, per calcolare la media.

Per ulteriori dettagli sulle metriche, leggi qui.

Apriamo la porta su Promtail:

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

Assicuriamo 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 è possibile scoprire, ad esempio, le quattro richieste più lente. Inoltre, è 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ò memorizzare i dati nel cloud, con chunk e indice memorizzati separatamente. Nella versione 1.5 è stata implementata la possibilità di memorizzare in un unico luogo, ma al momento non è consigliabile utilizzarla in produzione.

Raccogliamo i log con Loki

I chunk possono essere memorizzati in uno storage compatibile S3, mentre per la memorizzazione degli indici si possono utilizzare database orizzontalmente scalabili: Cassandra, BigTable o DynamoDB. Altre parti di Loki — Distributors (per la scrittura) e Querier (per le query) — sono senza stato e scalabili 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% rispetto alla dimensione complessiva: "Come Loki Correlates Metrics and Logs — E Ti Fa Risparmiare”.

Confronto tra Loki ed ELK

Dimensione dell'indice

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

Esempio di due righe dal log:

Raccogliamo i log con Loki

Nell'indicizzazione ELK, ciò ha comportato una dimensione dell'indice di 30,3 MB:

Raccogliamo i log con Loki

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

Consumo di memoria

Raccogliamo i log con Loki

Se confrontiamo l'intero stack Prometheus e ELK, Loki "mangia" diverse volte meno. È chiaro che un servizio in Go consuma meno di un servizio in Java, e il confronto tra la dimensione della JVM Heap di Elasticsearch e la memoria dedicata per Loki non è corretto, ma va notato che Loki utilizza molta meno memoria. Il suo vantaggio in termini di CPU non è così evidente, ma c'è comunque.

Velocità

Loki consuma i log più rapidamente. La velocità dipende da molti fattori: quali log si usano, quanto siamo sofisticati nel parsing, rete, disco, ecc. - ma è sicuramente superiore a quella di ELK (nel mio test era circa il doppio). Questo è spiegato dal fatto che Loki inserisce molti meno dati nell'indice e, di conseguenza, impiega meno tempo nell'indicizzazione. D'altra parte, la situazione è opposta per la velocità di ricerca: Loki rallenta sensibilmente con dati di dimensioni superiori a qualche gigabyte, mentre la velocità di ricerca di ELK non dipende dalla dimensione dei dati.

Ricerca nei log

Loki è nettamente inferiore a ELK per quanto riguarda le capacità di ricerca nei log. Grep con espressioni regolari è uno strumento potente, ma non si avvicina a un database maturo. L'assenza di query range, aggregazione solo per etichette, impossibilità di cercare senza etichette: tutto ciò limita la nostra capacità di cercare informazioni interessanti in Loki. Ciò non implica che con Loki non si possa trovare nulla, ma definisce il flusso di lavoro con i log, dove prima si identifica un problema nei grafici di Prometheus e poi si cercano informazioni nei log utilizzando quelle etichette.

Document

Innanzitutto, è bello (scusate, non ho potuto resistere). Grafana ha un'interfaccia piacevole, ma Kibana è molto più funzionale.

Pro e contro di Loki

Tra i lati positivi si può notare che Loki si integra con Prometheus, quindi otteniamo metriche e avvisi pronti all'uso. È comodo per la raccolta e l'archiviazione dei log con i Pods di Kubernetes, poiché eredita da Prometheus la discovery dei servizi e aggiunge automaticamente le etichette.

Tra i lati negativi c'è una documentazione scadente. Alcune cose, come le peculiarità e le funzionalità di Promtail, le ho scoperte solo durante l'analisi del codice, ma per fortuna è open-source. Un altro svantaggio sono le limitate capacità di parsing. Ad esempio, Loki non riesce a gestire i log multilinea. Tra i difetti, si può anche includere il fatto che Loki è una tecnologia relativamente giovane (il rilascio 1.0 è avvenuto nel novembre 2019).

Conclusione

Loki è una tecnologia al 100% interessante, che si adatta a progetti piccoli e medi, consentendo di affrontare una serie di compiti relativi all'aggregazione, alla ricerca e all'analisi dei log.

Non utilizziamo Loki in Badoo, poiché abbiamo un stack ELK che ci soddisfa e che nel corso degli anni si è arricchito di varie soluzioni personalizzate. Per noi, il problema principale è la ricerca nei log. Avere quasi 100 GB di log al giorno rende fondamentale trovare tutto e qualcosa in più velocemente. Per costruire grafici e monitoraggio utilizziamo altre soluzioni, che sono adattate alle nostre esigenze e integrate tra loro. L'ecosistema di Loki ha alcuni vantaggi tangibili, ma non ci offrirà più di quanto già abbiamo, e i suoi vantaggi non compensano sicuramente il costo della migrazione.

E sebbene dopo la ricerca sia 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