Dacă ați folosit vreodată interfețe web pentru a vizualiza jurnalele, cu siguranță ați observat cât de adesea aceste interfețe sunt voluminoase și (adesea) nu foarte convenabile sau responsive. Unele se pot adapta cu greu, altele sunt complet teribile, dar, după părerea mea, cauza tuturor problemelor constă în modul greșit în care abordăm sarcina vizualizării jurnalelor: încercăm să creăm o interfață web acolo unde CLI (interfața de linie de comandă) funcționează mai bine. Mie personal îmi place foarte mult să lucrez cu tail, grep, awk și altele, așa că pentru mine interfața ideală pentru lucrul cu jurnale ar fi ceva asemănător lui tail și grep, dar care poate fi utilizat pentru a citi jurnale care provin de pe mai multe servere. Adică, bineînțeles, să le citesc din ClickHouse!
*din părerile personale ale unui utilizator Habr
Faceți cunoștință cu logscli
Nu mi-am ales un nume pentru interfața mea și, ca să fiu sincer, aceasta există mai degrabă ca un prototip, dar dacă doriți să vizionați direct sursele, bine ați venit: (350 de linii de cod selectat pe Go).
Funcționalități
Am avut ca obiectiv să creez o interfață care să fie familiară celor care sunt obișnuiți cu tail/grep, adică să suporte următoarele lucruri:
- Vizualizarea tuturor jurnalelor, fără filtrare.
- Păstrarea liniilor care conțin un substring fix (flag
-Fugrep). - Păstrarea liniilor care corespund unei expresii regulate (flag
-Eugrep). - În mod implicit, vizualizarea se face în ordine cronologică inversă, deoarece de obicei ne interesează în primul rând cele mai recente jurnale.
- Afișarea contextului de lângă fiecare linie (parametrii
-A,-Bși-Cugrep, care tipăresc N linii înainte, după și în jurul fiecărei linii care se potrivește, respectiv). - Vizualizarea jurnalelor primite în timp real, cu filtrare sau fără (de fapt
tail -f | grep). - Interfața trebuie să fie compatibilă cu
less,head,tailși altele — rezultatele ar trebui să fie returnate fără limitări de număr; liniile sunt tipărite în flux atâta timp cât utilizatorul este interesat să le primească; semnalulSIGPIPEar trebui să întrerupă în tăcere streaming-ul jurnalelor, la fel cum factail,grepși celelalte utilitare UNIX.
Implementarea
Voi presupune că deja știți cum să livrați jurnalele către ClickHouse. Dacă nu, vă recomand să încercați și , dar și pentru .
Pentru început, trebuie să ne stabilim schema bazei de date. Deoarece, de obicei, vrem să primim jurnalele sortate în funcție de timp, este logic să le stocăm astfel. Dacă există mai multe categorii de jurnale și toate sunt de același tip, putem face categoria jurnale ca prima coloană a cheii primare - acest lucru va permite să avem un singur tabel în loc de mai multe, ceea ce va fi un mare avantaj la inserarea în ClickHouse (pe serverele cu unități de disc mecanice se recomandă inserarea datelor nu mai des de aproximativ o dată pe secundă. pentru întreg serverul).
Deci, avem nevoie de următoarea schemă de tabele:
CREATE TABLE logs(
category LowCardinality(String), -- categoria jurnale (opțional)
time DateTime, -- timpul evenimentului
millis UInt16, -- milisecunde (poate fi și microsecunde, etc.): se recomandă stocarea acestora, dacă sunt multe evenimente, pentru a putea distinge mai ușor între ele
..., -- câmpurile dumneavoastră, de exemplu numele serverului, nivelul de logare etc.
message String -- textul mesajului
) ENGINE=MergeTree()
ORDER BY (category, time, millis)Din păcate, nu am reușit să găsesc din prima surse deschise cu jurnale realiste care să poată fi descărcate, așa că am folosit în schimb pentru exemplu . Desigur, structura acestora nu este aceeași cu cea a jurnalelor text, dar pentru ilustrare acest lucru nu este esențial.
instrucțiuni pentru încărcarea recenziilor Amazon în ClickHouse
Să creăm tabelul:
CREATE TABLE amazon(
review_date Date,
time DateTime DEFAULT toDateTime(toUInt32(review_date) * 86400 + rand() % 86400),
millis UInt16 DEFAULT rand() % 1000,
marketplace LowCardinality(String),
customer_id Int64,
review_id String,
product_id LowCardinality(String),
product_parent Int64,
product_title String,
product_category LowCardinality(String),
star_rating UInt8,
helpful_votes UInt32,
total_votes UInt32,
vine FixedString(1),
verified_purchase FixedString(1),
review_headline String,
review_body String
)
ENGINE=MergeTree()
ORDER BY (time, millis)
SETTINGS index_granularity=8192Datasetul Amazon are doar data pentru recenzie, dar nu există o oră exactă, așa că vom completa aceste date cu un număr aleator.
Nu este necesar să descărcați toate fișierele tsv și puteți să vă limitați la primele ~10-20, pentru a obține deja un set de date suficient de mare care să nu încapă în 16 GB de memorie RAM. Pentru încărcarea fișierelor TSV am folosit următoarea comandă:
for i in *.tsv; do
echo $i;
tail -n +2 $i | pv |
clickhouse-client --input_format_allow_errors_ratio 0.5 --query='INSERT INTO amazon(marketplace,customer_id,review_id,product_id,product_parent,product_title,product_category,star_rating,helpful_votes,total_votes,vine,verified_purchase,review_headline,review_body,review_date) FORMAT TabSeparated'
donePe un disk persistent standard (HDD) de 1000 GB în Google Cloud (am ales această dimensiune pentru a avea puțin mai multă viteză, deși, poate, un SSD de dimensiunea necesară ar fi fost mai ieftin) viteza de încărcare a fost de aproximativ 75 MB/sec pe 4 nuclee.
- Trebuie să menționez că lucrez la Google, dar am folosit un cont personal și acest articol nu are legătură cu munca mea în companie.
Toate ilustrațiile le voi realiza exact pe acest set de date, pentru că aceasta este tot ce am avut la îndemână.
Afișarea progresului scanării datelor
Deoarece în ClickHouse vom utiliza scanarea completă a tabelei cu jurnale, iar această operațiune poate dura mult timp și nu va returna rezultate prea repede dacă sunt puține potriviri, este de dorit să putem afișa progresul execuției interogării până la obținerea primelor rânduri cu rezultatul. Pentru aceasta, în interfața HTTP există un parametru care permite trimiterea progresului în antetele HTTP: send_progress_in_http_headers=1. Din păcate, biblioteca standard Go nu poate citi antetele pe măsură ce acestea sunt primite, dar interfața HTTP 1.0 (nu confundați cu 1.1!) este acceptată de ClickHouse, așa că putem deschide o conexiune TCP brută cu ClickHouse, trimițându-i GET \/?query=... HTTP\/1.0nn și primi în răspuns antetele și corpul răspunsului fără nicio escape și criptare, astfel încât, în acest caz, nu avem nevoie de biblioteca standard.
Streaming-ul jurnalele din ClickHouse
În ClickHouse există deja de relativ mult timp (din 2019?) o optimizare pentru interogările cu ORDER BY, astfel că o interogare de forma
SELECT time, millis, message
FROM logs
WHERE message LIKE '%something%'
ORDER BY time DESC, millis DESCva începe să returneze imediat rândurile care au în message substring-ul "something", fără a aștepta finalizarea scanării.
De asemenea, ar fi foarte convenabil ca ClickHouse să anuleze singur interogarea când conexiunea este închisă, dar acest comportament nu este implicit. Anularea automată a interogării poate fi activată cu opțiunea cancel_http_readonly_queries_on_client_close=1.
Prelucrarea corectă a SIGPIPE în Go
Când executați, să zicem, comanda some_cmd | head -n 10, cum exact termină comanda some_cmd execuția sa când head a citit 10 rânduri? Răspunsul este simplu: când head se finalizează, pipe-ul se închide, iar stdout comenzii some_cmd începe să indice, în mod condiționat, „nicăieri”. Când some_cmd încearcă să scrie în pipe-ul închis, .
În Go, acest lucru se întâmplă în mod implicit, dar handlerul pentru semnalul SIGPIPE la final afișează de asemenea "signal: SIGPIPE" sau un mesaj similar, iar pentru a elimina acest mesaj, trebuie să gestionăm noi SIGPIPE așa cum dorim, adică să ieșim pur și simplu în liniște:
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGPIPE)
go func() {
<-ch
os.Exit(0)
}()Afișarea contextului mesajului
Adesea, dorim să vedem contextul în care a avut loc o anumită eroare (de exemplu, ce cerere a provocat panică sau ce probleme conexe au fost vizibile înainte de cădere), și în grep acest scop servesc opțiunile -A, -B și -C, care arată un anumit număr de linii după, înainte și în jurul mesajului, respectiv.
Din păcate, nu am găsit o modalitate simplă de a face același lucru în ClickHouse, prin urmare, pentru a afișa contextul, pentru fiecare linie a rezultatului se trimite o cerere suplimentară de forma aproximativă (detalii depind de sortare și de răspunsul pe care trebuie să îl arătăm înainte sau după):
SELECT time, millis, review_body FROM amazon
WHERE (time = 'TIMP_EVENT' AND millis < MILISECUNDE_EVENT) OR (time < 'TIMP_EVENT')
ORDER BY time DESC, millis DESC
LIMIT NUMĂ_LINII_CONTEXT
SETTINGS max_threads=1Deoarece cererea este trimisă aproape imediat după ce ClickHouse a returnat linia corespunzătoare, aceasta ajunge în cache și, în general, cererea se execută destul de repede și consumă ușor CPU (de obicei, cererea durează aproximativ ~6 ms pe mașina mea virtuală).
Afișarea mesajelor noi în timp real
Pentru a arăta mesajele sosite în timp (aproape) real, pur și simplu executăm cererea la câteva secunde, amintindu-ne ultimul timestamp pe care l-am întâlnit anterior.
Exemple de comenzi
Cum arată comenzile tipice logscli în practică?
Dacă ați încărcat setul de date Amazon, pe care l-am menționat la începutul articolului, atunci puteți executa următoarele comenzi:
# Показать строки, где встречается слово walmart
$ logscli -F 'walmart' | less
# Показать самые свежие 10 строк, где встречается "terrible"
$ logscli -F terrible -limit 10
# То же самое без -limit:
$ logscli -F terrible | head -n 10
# Показать все строки, подходящие под /times [0-9]/, написанные для vine и у которых высокий рейтинг
$ logscli -E 'times [0-9]' -where="vine='Y' AND star_rating>4" | less
# Показать все строки со словом "panic" и 3 строки контекста вокруг
$ logscli -F 'panic' -C 3 | less
# Непрерывно показывать новые строки со словом "5-star"
$ logscli -F '5-star' -tailfLinkuri
Codul utilitarului (fără documentație) este disponibil pe github la adresa . Aș fi bucuros să aud gândurile dvs. despre ideea mea pentru o interfață de consolă pentru vizualizarea logurilor bazată pe ClickHouse.
Sursa: habr.com
