Palkide korjamine Lokilt

Palkide korjamine Lokilt

Meie Badoos jälgime pidevalt uusi tehnoloogiaid ja hindame, kas neid oma süsteemis kasutada või mitte. Soovime üht neist uuringutest kogukonnaga jagada. See on pühendatud Lokile, logide koondamissüsteemile.

Loki on lahendus logide salvestamiseks ja vaatamiseks ning see virn pakub ka paindlikku süsteemi nende analüüsimiseks ja andmete Prometheusele saatmiseks. Mais ilmus veel üks värskendus, mida loojad aktiivselt reklaamivad. Meid huvitas, mida Loki teha suudab, milliseid funktsioone see pakub ja mil määral saab see alternatiivina ELK-le, praegu kasutatavale pinule.

Mis on Loki

Grafana Loki on komponentide komplekt tervikliku logisüsteemi jaoks. Erinevalt teistest sarnastest süsteemidest põhineb Loki ideel indekseerida ainult logide metaandmeid - silte (nagu Prometheuses) ja tihendada logid ise kõrvuti eraldi tükkideks.

Домашняя страница, GitHub

Enne kui hakkan arutama, mida saate Lokiga teha, tahan selgitada, mida tähendab "ainult metaandmete indekseerimise idee". Võrdleme Loki lähenemisviisi ja indekseerimismeetodit traditsioonilistes lahendustes, nagu Elasticsearch, kasutades nginxi logi rea näidet:

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

Traditsioonilised süsteemid sõeluvad kogu rea, sealhulgas väljad, millel on palju kordumatuid user_id ja item_id väärtusi, ning salvestavad kõik suurtes indeksites. Selle lähenemisviisi eeliseks on see, et saate kiiresti käivitada keerulisi päringuid, kuna peaaegu kõik andmed on registris. Kuid selle eest peate maksma, kuna indeks muutub suureks, mis tähendab mälunõudeid. Sellest tulenevalt on palkide täistekstiindeks suuruselt võrreldav logide endaga. Selle kiireks otsimiseks tuleb indeks mällu laadida. Ja mida rohkem logisid, seda kiiremini indeks suureneb ja seda rohkem mälu kulub.

Loki lähenemine nõuab, et stringist eraldataks ainult vajalikud andmed, mille väärtuste arv on väike. Nii saame väikese indeksi ja saame otsida andmeid, filtreerides need aja ja indekseeritud väljade järgi ning seejärel skannides ülejäänud regulaaravaldiste või alamstringi otsingutega. Protsess ei tundu kõige kiirem, kuid Loki jagab päringu mitmeks osaks ja täidab need paralleelselt, töödeldes lühikese aja jooksul suure hulga andmeid. Nendes olevate kildude ja paralleelsete päringute arv on seadistatav; seega sõltub ajaühikus töödeldavate andmete hulk lineaarselt pakutavate ressursside hulgast.

See kompromiss suure kiire indeksi ja väikese paralleelse brute-force indeksi vahel võimaldab Lokil süsteemi kulusid kontrollida. Seda saab vastavalt teie vajadustele paindlikult konfigureerida ja laiendada.

Loki stäkk koosneb kolmest komponendist: Promtail, Loki, Grafana. Promtail kogub logisid, töötleb neid ja saadab Lokile. Loki hoiab neid. Ja Grafana saab Lokilt andmeid küsida ja neid näidata. Üldiselt saab Loki kasutada mitte ainult palkide hoidmiseks ja nende kaudu otsimiseks. Kogu pinu pakub suurepäraseid võimalusi sissetulevate andmete töötlemiseks ja analüüsimiseks Prometheuse meetodil.
Paigaldusprotsessi kirjelduse leiate siin.

Logi otsing

Logidest saate otsida spetsiaalses liideses Grafana — Explorer. Päringutes kasutatakse LogQL-i keelt, mis on väga sarnane Prometheuse kasutatavale PromQL-ile. Põhimõtteliselt võib seda pidada hajutatud grepiks.

Otsingu liides näeb välja selline:

Palkide korjamine Lokilt

Päring ise koosneb kahest osast: selektor ja filter. Selektor on otsing indekseeritud metaandmete (siltide) järgi, mis on määratud logidele, ja filter on otsingustring või regexp, mis filtreerib välja valija määratud kirjed. Antud näites: lokkis sulgudes - valija, kõik pärast - filter.

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

Loki tööviisi tõttu ei saa te päringuid teha ilma valijata, kuid silte saab muuta suvaliselt üldiseks.

Valija on lokkis sulgudes oleva väärtuse võtmeväärtus. Saate kombineerida valijaid ja määrata erinevaid otsingutingimusi, kasutades operaatoreid =, != või regulaaravaldisi:

{instance=~"kafka-[23]",name!="kafka-dev"} 
// Найдёт логи с лейблом instance, имеющие значение kafka-2, kafka-3, и исключит dev 

Filter on tekst või regexp, mis filtreerib välja kõik valija saadud andmed.

Mõõdikute režiimis on võimalik saada vastuvõetud andmete põhjal ad-hoc graafikuid. Näiteks saate indeksi stringi sisaldava kirje esinemissageduse nginxi logides teada saada:

Palkide korjamine Lokilt

Funktsioonide täieliku kirjelduse leiate dokumentatsioonist LogQL.

Logi parsimine

Logide kogumiseks on mitu võimalust:

  • Palkide kogumise virna standardkomponendi Promtaili abiga.
  • Otse dokkimiskonteinerist kasutades Loki Dockeri logimisdraiver.
  • Kasutage Fluentd või Fluent Bit, mis saab Lokile andmeid saata. Erinevalt Promtailist on neil valmis parserid peaaegu igat tüüpi logide jaoks ja saavad hakkama ka mitmerealiste logidega.

Tavaliselt kasutatakse parsimiseks Promtaili. See teeb kolme asja:

  • Otsib andmeallikaid.
  • Kinnitage neile sildid.
  • Saadab andmed Lokile.

Praegu saab Promtail lugeda logisid kohalikest failidest ja systemd ajakirjast. See tuleb paigaldada igale masinale, kust palke kogutakse.

Kubernetesiga on integreeritud: Promtail tuvastab Kubernetes REST API kaudu automaatselt klastri oleku ja kogub logid sõlmest, teenusest või kaustast, postitades kohe Kubernetese metaandmetel põhinevad sildid (podi nimi, faili nimi jne).

Pipeline'i abil saate ka logi andmete põhjal silte riputada. Pipeline Promtail võib koosneda nelja tüüpi etapist. Täpsem info - sisse ametlik dokumentatsioon, märgin kohe mõned nüansid.

  1. Parsimise etapid. See on RegExi ja JSONi etapp. Selles etapis eraldame logidest andmed nn väljavõetud kaardile. Saate JSON-ist ekstraheerida, kopeerides meile vajalikud väljad ekstraheeritud kaardile või regulaaravaldiste (RegEx) abil, kus nimega rühmad kaardistatakse ekstraheeritud kaardile. Ekstraheeritud kaart on võtmeväärtuste salvestusruum, kus võti on välja nimi ja väärtus on selle väärtus logidest.
  2. Transformatsiooni etapid. Sellel etapil on kaks võimalust: teisendus, kus me määrame teisendusreeglid, ja allikas – ekstraheeritud kaardilt teisenduse andmeallikas. Kui ekstraheeritud kaardil sellist välja pole, siis see luuakse. Seega on võimalik luua silte, mis ei põhine väljavõetud kaardil. Selles etapis saame ekstraheeritud kaardil olevate andmetega manipuleerida, kasutades üsna võimsat golangi mall. Lisaks peame meeles pidama, et eraldatud kaart laaditakse sõelumise ajal täielikult, mis võimaldab näiteks kontrollida selles olevat väärtust: “{{if .tag}tag väärtus eksisteerib{end}}”. Mall toetab tingimusi, silmuseid ja mõningaid stringifunktsioone, nagu Asenda ja Kärbi.
  3. Tegevuse etapid. Selles etapis saate ekstraheeritud materjaliga midagi ette võtta:
    • Looge ekstraheeritud andmetest silt, mille Loki indekseerib.
    • Muutke või määrake sündmuse aeg logist.
    • Muutke Lokile minevaid andmeid (logiteksti).
    • Loo mõõdikud.
  4. Filtreerimise etapid. Sobitamisetapp, kus saame saata kirjed, mida me ei pea määrama /dev/null, või saata need edasiseks töötlemiseks.

Kasutades tavaliste nginxi logide töötlemise näidet, näitan, kuidas saate Promtaili abil logisid sõeluda.

Testi jaoks võtame modifitseeritud nginx jwilder/nginx-proxy:alpine kujutise ja väikese deemoni, mis suudab endalt HTTP kaudu päringuid teha nginx-puhverserverina. Deemonil on mitu lõpp-punkti, millele see võib anda erineva suurusega, erineva HTTP oleku ja erineva viivitusega vastuseid.

Kogume palke dokkekonteineritelt, mille leiab tee /var/lib/docker/containers/ / -json.log

Docker-compose.yml seadistame Promtaili ja määrame konfiguratsiooni tee:

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'
 // ...

Lisage logide tee aadressi promtail.yml (konfiguratsioonis on valik "docker", mis teeb sama ühes reas, kuid see poleks nii ilmne):

scrape_configs:
 - job_name: containers

   static_configs:
       labels:
         job: containerlogs
         __path__: /var/lib/docker/containers/*/*log  # for linux only

Kui see konfiguratsioon on lubatud, saab Loki logid kõikidest konteineritest. Selle vältimiseks muudame failis docker-compose.yml test nginxi sätteid - lisage sildiväljale logimine:

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

Redigeerige promtail.yml ja seadistage Pipeline. Logid on järgmised:

{"log":"u001b[0;33;1mnginx.1    | u001b[0mnginx.test 172.28.0.3 - - [13/Jun/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/Jun/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"}

torujuhtme etapid:

 - json:
     expressions:
       stream: stream
       attrs: attrs
       tag: attrs.tag

Eraldame sissetulevast JSON-ist väljad voog, attrs, attrs.tag (kui need on olemas) ja lisame need ekstraktitud kaardile.

 - regex:
     expression: ^(?P<image_name>([^|]+))|(?P<container_name>([^|]+))$
     source: "tag"

Kui ekstraheeritud kaardile oli võimalik panna sildiväli, siis regexpi abil eraldame pildi ja konteineri nimed.

 - labels:
     image_name:
     container_name:

Määrame sildid. Kui ekstraktitud andmetest leitakse võtmed pildi_nimi ja konteineri_nimi, määratakse nende väärtused vastavatele siltidele.

 - match:
     selector: '{job="docker",container_name="",image_name=""}'
     action: drop

Loobume kõik logid, millel pole määratud silte pildi_nimi ja konteineri_nimi.

  - match:
     selector: '{image_name="nginx.promtail.test"}'
     stages:
       - json:
           expressions:
             row: log

Kõigi logide puhul, mille pildi_nimi on võrdne väärtusega nginx.promtail.test, eraldame logivälja lähtelogist ja lisame selle reaklahviga ekstraktitud kaardile.

  - regex:
         # suppress forego colors
         expression: .+nginx.+|.+[0m(?P<virtual_host>[a-z_.-]+) +(?P<nginxlog>.+)
         source: logrow

Tühjendame sisendstringi regulaaravaldistega ja tõmbame välja nginxi virtuaalse hosti ja nginxi logirea.

     - regex:
         source: nginxlog
         expression: ^(?P<ip>[w.]+) - (?P<user>[^ ]*) [(?P<timestamp>[^ ]+).*] "(?P<method>[^ ]*) (?P<request_url>[^ ]*) (?P<request_http_protocol>[^ ]*)" (?P<status>[d]+) (?P<bytes_out>[d]+) "(?P<http_referer>[^"]*)" "(?P<user_agent>[^"]*)"( "(?P<response_time>[d.]+)")?

Parsi nginxi logi regulaaravaldistega.

    - regex:
           source: request_url
           expression: ^.+.(?P<static_type>jpg|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<photo>[^/?.]+).*$
       - regex:
           source: request_url
           expression: ^/api/(?P<api_request>[^/?.]+).*$

Parsi päringu_url. Regexpi abil määrame päringu eesmärgi: staatikale, fotodele, API-le ja määrame väljavõetud kaardil vastava võtme.

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

Kasutades mallis tingimuslikke operaatoreid, kontrollime ekstraheeritud kaardil installitud välju ja määrame väljale request_type nõutavad väärtused: foto, staatiline, API. Ebaõnnestumise korral määrake muu. Nüüd sisaldab päringu_tüüp päringu tüüpi.

       - labels:
           api_request:
           virtual_host:
           request_type:
           status:

Määrasime sildid api_request, virtual_host, request_type ja status (HTTP staatus) selle põhjal, mida meil õnnestus ekstraheeritud kaardile lisada.

       - output:
           source: nginx_log_row

Muutke väljundit. Nüüd läheb ekstraheeritud kaardilt puhastatud nginxi logi Lokile.

Palkide korjamine Lokilt

Pärast ülaltoodud konfiguratsiooni käivitamist näete, et iga kirje on märgistatud logi andmete põhjal.

Pidage meeles, et suure hulga väärtustega (kardinaalsus) siltide eraldamine võib Loki oluliselt aeglustada. See tähendab, et te ei tohiks indeksisse panna näiteks kasutaja_id. Lisateavet selle kohta leiate artiklistKuidas saavad Loki sildid logipäringuid kiiremaks ja lihtsamaks muuta". Kuid see ei tähenda, et te ei saaks otsida kasutaja_id järgi ilma indeksiteta. Otsimisel on vaja kasutada filtreid (andmete järgi "haara") ja siinne register toimib voo identifikaatorina.

Logi visualiseerimine

Palkide korjamine Lokilt

Loki võib toimida LogQL-i abil Grafana diagrammide andmeallikana. Toetatakse järgmisi funktsioone:

  • kiirus - kirjete arv sekundis;
  • count over time – kirjete arv antud vahemikus.

Samuti on koondamisfunktsioonid Sum, Avg ja teised. Saate koostada üsna keerukaid graafikuid, näiteks HTTP-vigade arvu graafiku:

Palkide korjamine Lokilt

Loki vaikeandmeallikas on natuke vähem funktsionaalne kui Prometheuse andmeallikas (näiteks ei saa legendi muuta), kuid Loki saab ühendada Prometheuse tüüpi allikana. Ma pole kindel, kas see on dokumenteeritud käitumine, kuid otsustades arendajate vastuse järgi "Kuidas konfigureerida Loki Prometheuse andmeallikaks? · Väljaanne #1222 · grafana/loki”, näiteks on see täiesti seaduslik ja Loki ühildub täielikult PromQL-iga.

Lisage Loki andmeallikana tüübiga Prometheus ja lisage URL /loki:

Palkide korjamine Lokilt

Ja saate teha graafikuid, nagu töötaksime Prometheuse mõõdikutega:

Palkide korjamine Lokilt

Arvan, et funktsionaalsuse lahknevus on ajutine ja arendajad parandavad selle tulevikus.

Palkide korjamine Lokilt

Mõõdikud

Loki pakub võimalust logidest numbrilisi mõõdikuid eraldada ja Prometheusele saata. Näiteks sisaldab nginxi logi baitide arvu vastuse kohta ja standardse logivormingu teatud muudatusega ka aega sekundites, mis kulus vastamiseks. Neid andmeid saab ekstraktida ja Prometheusele saata.

Lisage saidile promtail.yml veel üks jaotis:

- match:
   selector: '{request_type="api"}'
   stages:
     - metrics:
         http_nginx_response_time:
           type: Histogram
           description: "response time 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: "response bytes sum"
           source: bytes_out
           config:
             action: add
         http_nginx_response_bytes_count:
           type: Counter
           description: "response bytes count"
           source: bytes_out
           config:
             action: inc

See valik võimaldab määrata ja värskendada mõõdikuid ekstraktitud kaardi andmete põhjal. Neid mõõdikuid ei saadeta Lokile – need kuvatakse lõpp-punktis Promtail /metrics. Prometheus peab olema konfigureeritud sellest etapist andmeid vastu võtma. Ülaltoodud näites kogume päringu_tüüp="api" jaoks histogrammi mõõdiku. Seda tüüpi mõõdikutega on mugav protsentiile hankida. Staatika ja fotode jaoks kogume keskmise arvutamiseks baitide summa ja ridade arvu, milles saime baite.

Lisateavet mõõdikute kohta siin.

Avage Promtailis port:

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

Veendume, et promtail_custom prefiksiga mõõdikud on ilmunud:

Palkide korjamine Lokilt

Prometheuse seadistamine. Lisa töökuulutus:

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

Ja joonistage graafik:

Palkide korjamine Lokilt

Nii saad teada näiteks neli kõige aeglasemat päringut. Samuti saate nende mõõdikute jälgimist konfigureerida.

Skaleerimine

Loki võib olla nii üksikbinaarrežiimis kui ka killustatud (horisontaalselt skaleeritav režiim). Teisel juhul saab see andmeid pilve salvestada ning tükid ja indeks salvestatakse eraldi. Versioonis 1.5 on ühes kohas salvestamise võimalus juurutatud, kuid tootmises seda veel kasutada ei soovita.

Palkide korjamine Lokilt

Tükke saab salvestada S3-ga ühilduvasse salvestusruumi, indeksite salvestamiseks kasutage horisontaalselt skaleeritavaid andmebaase: Cassandra, BigTable või DynamoDB. Loki muud osad – Levitajad (kirjutamiseks) ja Querier (päringute jaoks) – on olekuta ja ka horisontaalselt skaleeritavad.

DevOpsDays Vancouver 2019 konverentsil teatas üks osalejatest Callum Styan, et koos Lokiga on tema projektil petabaite palke, mille indeks on alla 1% kogumahust: “Kuidas Loki mõõdikuid ja logisid korreleerib – ja säästab teie raha".

Loki ja ELK võrdlus

Indeksi suurus

Saadud indeksi suuruse testimiseks võtsin logid nginxi konteinerist, mille jaoks ülaltoodud Pipeline oli konfigureeritud. Logifail sisaldas 406 624 rida kogumahuga 109 MB. Logid loodi tunni jooksul, umbes 100 kirjet sekundis.

Näide kahest reast logist:

Palkide korjamine Lokilt

ELK indekseerides andis see indeksi suuruseks 30,3 MB:

Palkide korjamine Lokilt

Loki puhul andis see tükkidena umbes 128 KB indeksit ja umbes 3,8 MB andmeid. Väärib märkimist, et logi loodi kunstlikult ja see ei sisaldanud väga erinevaid andmeid. Lihtne gzip algse Dockeri JSON-logi andmetega andis tihenduseks 95,4% ja arvestades, et Lokile saadeti ainult puhastatud nginxi logi, on tihendamine 4 MB-ni arusaadav. Loki siltide unikaalsete väärtuste koguarv oli 35, mis seletab indeksi väiksust. ELK jaoks sai ka palgi koristatud. Nii tihendas Loki algandmeid 96% ja ELK 70%.

Mälu tarbimine

Palkide korjamine Lokilt

Kui võrrelda kogu Prometheuse ja ELK stäkki, siis Loki "sööb" kordades vähem. On selge, et Go-teenus tarbib vähem kui Java-teenus ning Heap Elasticsearch JVM-i suuruse ja Loki jaoks eraldatud mälu võrdlemine on vale, kuid sellegipoolest väärib märkimist, et Loki kasutab palju vähem mälu. Selle protsessori eelis pole nii ilmne, kuid see on ka olemas.

Kiirus

Loki "õgib" palke kiiremini. Kiirus sõltub paljudest teguritest - millistest logidest, kui keerukalt neid sõelume, võrk, ketas jne -, kuid see on kindlasti suurem kui ELK-l (minu testis - umbes kaks korda). Seda seletatakse asjaoluga, et Loki paneb indeksisse palju vähem andmeid ja kulutab seetõttu indekseerimisele vähem aega. Otsingukiirusega on sel juhul olukord vastupidine: Loki aeglustub märgatavalt mõnest gigabaidist suurematel andmetel, samas kui ELK puhul ei sõltu otsingukiirus andmemahust.

Logi otsing

Loki jääb logiotsingu võimekuse poolest ELK-le oluliselt alla. Regulaaravaldistega Grep on tugev asi, kuid jääb alla täiskasvanute andmebaasile. Vahemikupäringute puudumine, koondamine ainult siltide järgi, võimetus otsida ilma siltideta - kõik see piirab meid Lokis huvipakkuva teabe otsimisel. See ei tähenda, et Loki abil midagi ei leita, kuid see määrab logidega töötamise voo, kui leiate esmalt Prometheuse diagrammidelt probleemi ja seejärel otsite nende siltide abil logidest, mis juhtus.

liides

Esiteks on see ilus (vabandust, ei suutnud vastu panna). Grafanal on kena välimusega liides, kuid Kibana on palju funktsionaalsem.

Loki plussid ja miinused

Plussidest võib märkida, et Loki integreerub vastavalt Prometheusega, saame mõõdikud ja hoiatamise karbist välja. See on mugav logide kogumiseks ja Kubernetes Podidega salvestamiseks, kuna sellel on Prometheuselt päritud teenusetuvastus ja see lisab sildid automaatselt.

Miinustest - halb dokumentatsioon. Mõned asjad, nagu Promtaili funktsioonid ja võimalused, avastasin alles koodi uurimise käigus, avatud lähtekoodiga eelised. Teine puudus on nõrk sõelumisvõimalus. Näiteks ei saa Loki sõeluda mitmerealisi logisid. Samuti on puuduste hulgas asjaolu, et Loki on suhteliselt noor tehnoloogia (väljalase 1.0 ilmus 2019. aasta novembris).

Järeldus

Loki on 100% huvitav tehnoloogia, mis sobib väikestele ja keskmistele projektidele, võimaldades lahendada paljusid palkide koondamise, palkide otsimise, jälgimise ja analüüsimise probleeme.

Lokit me Badoos ei kasuta, sest meil on meile sobiv ELK stäkk, mis on aastate jooksul erinevate kohandatud lahendustega üle kasvanud. Meie jaoks on komistuskiviks otsimine palkidest. Ligi 100 GB logiga päevas on meie jaoks oluline, et suudaksime kõik ja natuke rohkemgi üles leida ning teha seda kiiresti. Diagrammi koostamiseks ja jälgimiseks kasutame muid lahendusi, mis on meie vajadustele kohandatud ja omavahel integreeritud. Loki pinal on käegakatsutavad eelised, kuid see ei anna meile rohkem kui see, mis meil on, ja selle eelised ei kaalu täpselt üles migratsiooni kulusid.

Ja kuigi pärast uurimistööd sai selgeks, et me ei saa Lokit kasutada, loodame, et see postitus aitab teid valiku tegemisel.

Artiklis kasutatud koodiga hoidla asub siin.

Allikas: www.habr.com

Lisa kommentaar