Zbieramy logi z Loki

Zbieramy logi z Loki

W Badoo nieustannie monitorujemy nowe technologie i oceniamy, czy warto je wdrożyć w naszym systemie. Chcielibyśmy się podzielić jednym z takich badań z naszą społecznością. Dotyczy ono Loki — systemu agregacji logów.

Loki to rozwiązanie do przechowywania i przeglądania logów, które oferuje również elastyczny system do ich analizy oraz przesyłania danych do Prometheus. W maju ukazała się kolejna aktualizacja, która jest aktywnie promowana przez twórców. Zainteresowało nas, co potrafi Loki, jakie możliwości oferuje i w jakim stopniu może być alternatywą dla ELK — stosu, którego obecnie używamy.

Co to jest Loki

Grafana Loki to zestaw komponentów do pełnoprawnego systemu pracy z logami. W przeciwieństwie do innych podobnych systemów, Loki opiera się na idei indeksowania tylko metadanych logów — labels (tak jak w Prometheus), podczas gdy same logi są kompresowane jako oddzielne kawałki.

Strona główna, GitHub

Zanim przejdziemy do opisu możliwości, jakie oferuje Loki, chciałbym wyjaśnić, co rozumiemy przez "ideę indeksowania tylko metadanych". Porównajmy podejście Loki z podejściem do indeksowania w tradycyjnych rozwiązaniach, takich jak Elasticsearch, na przykładzie wpisu z logu nginx:

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"

Tradycyjne systemy parsują całą linię, w tym pola z dużą liczbą unikalnych wartości user_id i item_id, i zapisują wszystko w dużych indeksach. Zaletą tego podejścia jest to, że można szybko wykonywać złożone zapytania, ponieważ prawie wszystkie dane znajdują się w indeksie. Jednak wiąże się to z tym, że indeks staje się duży, co prowadzi do wymagań pamięciowych. W rezultacie pełnotekstowy indeks logów jest wielkości porównywalnej z samymi logami. Aby szybko przeszukiwać, indeks musi być załadowany w pamięci. Im więcej logów, tym szybciej rośnie indeks i tym więcej pamięci zużywa.

Podejście Loki wymaga, aby z ciągu zostały wydobyte tylko niezbędne dane, których liczba jest niewielka. W ten sposób uzyskujemy mały indeks i możemy przeszukiwać dane, filtrując je według czasu oraz zindeksowanych pól, a następnie skanując pozostałe za pomocą wyrażeń regularnych lub wyszukiwania podciągów. Proces wydaje się nie najszybszy, ale Loki dzieli zapytanie na kilka części i wykonuje je równolegle, przetwarzając dużą ilość danych w krótkim czasie. Liczba shardów i równoległych zapytań w nich jest konfigurowana; w ten sposób liczba danych, które można przetworzyć w jednostce czasu, jest liniowo zależna od liczby dostarczonych zasobów.

Ten kompromis między dużym szybkim indeksem a małym indeksem z równoległym pełnym przeszukiwaniem pozwala Loki kontrolować koszty systemu. Można go elastycznie dostosować i rozszerzać w zależności od potrzeb.

Stos Loki składa się z trzech komponentów: Promtail, Loki, Grafana. Promtail zbiera logi, przetwarza je i wysyła do Loki. Loki je przechowuje. A Grafana potrafi zapytywać dane z Loki i je wyświetlać. Ogólnie rzecz biorąc, Loki można używać nie tylko do przechowywania logów i ich przeszukiwania. Cały stos daje ogromne możliwości przetwarzania i analizy napływających danych, wykorzystując sposób Prometheusa.
Opis procesu instalacji można znaleźć tutaj.

Wyszukiwanie w logach

Wyszukiwanie w logach można przeprowadzać w specjalnym interfejsie Grafana — Explorer. Do zapytań używa się języka LogQL, bardzo podobnego do PromQL używanego w Prometheusie. W zasadzie można go rozpatrywać jako rozproszony grep.

Interfejs wyszukiwania wygląda tak:

Zbieramy logi z Loki

Same zapytanie składa się z dwóch części: selector i filter. Selector to wyszukiwanie po zindeksowanych metadanych (etykietach), które są przypisane do logów, a filter to ciąg wyszukiwania lub regexp, za pomocą którego filtruje się rekordy określone przez selektor. W podanym przykładzie: W nawiasach klamrowych — selektor, wszystko, co po — filtr.

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

Z powodu zasady działania Loki nie można wykonywać zapytań bez selektora, ale etykiety można czynić dowolnie ogólnymi.

Selektor to wartości key-value w nawiasach klamrowych. Można łączyć selektory i ustawiać różne warunki wyszukiwania, używając operatorów =, != lub wyrażeń regularnych:

{instance=~"kafka-[23]",name!="kafka-dev"} 
// Znajdzie logi z etykietą instance, mające wartość kafka-2, kafka-3, i wykluczy dev 

Filtr to tekst lub wyrażenie regularne, które przefiltruje wszystkie dane uzyskane przez selektor.

Istnieje możliwość uzyskania ad-hoc wykresów na podstawie zebranych danych w trybie metrics. Na przykład, można poznać częstotliwość występowania w logach nginx wpisu zawierającego ciąg index:

Zbieramy logi z Loki

Pełny opis możliwości można znaleźć w dokumentacji LogQL.

Parsowanie logów

Istnieje kilka sposobów zbierania logów:

  • Za pomocą Promtail, standardowego komponentu stosu do zbierania logów.
  • Bezpośrednio z kontenera dockera przy pomocy Oprogramowania Loki Docker Logging Driver.
  • Można użyć Fluentd lub Fluent Bit, które potrafią wysyłać dane do Loki. W przeciwieństwie do Promtail mają gotowe parsery praktycznie do każdego rodzaju logu i radzą sobie również z logami multiline.

Zazwyczaj do parsowania używa się Promtail. Wykonuje on trzy rzeczy:

  • Znajduje źródła danych.
  • Przypina do nich etykiety.
  • Wysyła dane do Loki.

Obecnie Promtail może czytać logi z lokalnych plików oraz z dziennika systemd. Musi być zainstalowany na każdej maszynie, z której zbierane są logi.

Jest integracja z Kubernetes: Promtail automatycznie przez Kubernetes REST API poznaje stan klastra i zbiera logi z węzła, usługi lub poda, jednocześnie przypinając etykiety na podstawie metadanych z Kubernetes (nazwa poda, nazwa pliku itp.).

Można również przypinać etykiety na podstawie danych z logu przy pomocy Pipeline. Pipeline Promtail może składać się z czterech typów etapów. Szczegóły można znaleźć oficjalnej dokumentacji, tutaj zwrócę uwagę na kilka szczegółów.

  1. Etapy parsowania. To etap RegEx i JSON. Na tym etapie wyciągamy dane z logów do tak zwanej extracted map. Można wyciągać z JSON, po prostu kopiując wymagane nam pola do extracted map, lub za pomocą wyrażeń regularnych (RegEx), gdzie w extracted map
  2. mapowane są grupy nazwane. Extracted map to magazyn key-value, gdzie key to nazwa pola, a value to jego wartość z logów.Etapy transformacji . Ten etap ma dwie opcje: transform, gdzie definiujemy zasady transformacji, oraz source — źródło danych do transformacji z extracted map. Jeśli w extracted map nie ma takiego pola, zostanie ono utworzone. W ten sposób można tworzyć etykiety, które nie są oparte na extracted map. Na tym etapie możemy manipulować danymi w extracted map, używając wystarczająco potężnegoPonadto należy pamiętać, że mapa extracted ładnie się ładowana podczas parsowania, co pozwala na przykład na sprawdzenie wartości: "{{if .tag}tag value exists{end}}". Szablon obsługuje warunki, pętle oraz niektóre funkcje tekstowe, takie jak Replace i Trim.
  3. Etapy działaniaNa tym etapie można coś zrobić z wypisanym:
    • Utworzyć etykietę z extracted data, która zostanie zindeksowana w Loki.
    • Zmień lub ustaw czas zdarzenia z logu.
    • Zmień dane (tekst logu), które zostaną przesłane do Loki.
    • Utwórz metryki.
  4. Etapy filtrowaniaEtap match, na którym można albo wysłać zapisy, które są nam niepotrzebne, do /dev/null, albo skierować je do dalszego przetwarzania.

Pokażę na przykładzie przetwarzania typowych logów nginx, jak można parsować logi za pomocą Promtail.

Na test weźmiemy zmodyfikowany obraz nginx jwilder/nginx-proxy:alpine jako nginx-proxy i małego demona, który potrafi pytać sam siebie przez HTTP. Demon ma kilka punktów końcowych, na które może przekazywać odpowiedzi różnej wielkości, z różnymi statusami HTTP i opóźnieniami.

Będziemy zbierać logi z kontenerów dockera, które można znaleźć pod ścieżką /var/lib/docker/containers//-json.log

W pliku docker-compose.yml konfigurujemy Promtail i wskazujemy ścieżkę do konfigu:

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

Dodajemy w promtail.yml ścieżkę do logów (w konfiguracji jest opcja "docker", która robi to samo w jednej linii, ale byłoby to mniej czytelne):

scrape_configs:
 - job_name: containers

   static_configs:
       labels:
         job: containerlogs
         __path__: /var/lib/docker/containers/*/*log  # tylko dla linuxa

Po włączeniu takiej konfiguracji logi ze wszystkich kontenerów będą trafiały do Loki. Aby tego uniknąć, zmieniamy ustawienia testowego nginx w docker-compose.yml — dodajemy pole logowania tag:

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

Poprawiamy promtail.yml i konfigurujemy Pipeline. Na wejściu znajdują się logi w następującym formacie:

{"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"}

Etap Pipeline:

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

Ekstrahujemy z przychodzącego JSON pola stream, attrs, attrs.tag (jeśli są) i umieszczamy je w mapie extracted.

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

Jeśli udało się umieścić pole tag w mapie extracted, za pomocą wyrażenia regularnego ekstrahujemy nazwy obrazu i kontenera.

 - labels:
     image_name:
     container_name:

Przypisujemy etykiety. Jeśli w danych extracted znajdą się klucze image_name i container_name, ich wartości zostaną przypisane odpowiednim etykietom.

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

Odrzucamy wszystkie logi, w których nie znaleziono ustawionych etykiet image_name i container_name.

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

Dla wszystkich logów, w których image_name wynosi nginx.promtail.test, ekstrahujemy z oryginalnego logu pole log i umieszczamy je w mapie extracted z kluczem row.

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

Oczyszczamy wejściowy ciąg za pomocą wyrażeń regularnych i wyciągamy virtual host nginx oraz ciąg logu nginx.

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

Parsujemy log nginx za pomocą wyrażeń regularnych.

    - 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[^/?.]+).*$

Analizujemy request_url. Za pomocą wyrażeń regularnych określamy przeznaczenie zapytania: do statyki, do zdjęć, do API i ustawiamy w mapie extracted odpowiedni klucz.

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

Za pomocą operatorów warunkowych w szablonie sprawdzamy ustawione pola w mapie extracted i ustalamy dla pola request_type odpowiednie wartości: photo, static, API. Ustawiamy other, jeśli się nie udało. Teraz request_type zawiera typ zapytania.

       - labels:
           api_request:
           virtual_host:
           request_type:
           status:

Ustawiamy etykiety api_request, virtual_host, request_type oraz status (status HTTP) na podstawie tego, co udało się umieścić w mapie extracted.

       - output:
           source: nginx_log_row

Zmiana output. Teraz w Loki wysyłany jest oczyszczony log nginx z mapy extracted.

Zbieramy logi z Loki

Po uruchomieniu podanego konfigu można zobaczyć, że każdemu wpisowi przypisano etykiety na podstawie danych z logu.

Należy pamiętać, że wydobywanie etykiet z dużą liczbą wartości (kardynalność) może znacznie spowolnić działanie Loki. To znaczy, że nie warto umieszczać w indeksie, na przykład, user_id. Więcej na ten temat można przeczytać w artykule “Jak etykiety w Loki mogą przyspieszyć i uprościć zapytania logów”. Ale to nie znaczy, że nie można szukać po user_id bez indeksów. Należy użyć filtrów podczas wyszukiwania („grepować” w danych), a indeks w tym przypadku działa jako identyfikator strumienia.

Wizualizacja logów

Zbieramy logi z Loki

Loki może pełnić rolę źródła danych dla wykresów Grafana, używając LogQL. Obsługiwane są następujące funkcje:

  • rate — liczba wpisów na sekundę;
  • count over time — liczba wpisów w określonym zakresie.

Dostępne są również funkcje agregacyjne Sum, Avg i inne. Można tworzyć dość skomplikowane wykresy, na przykład wykres liczby błędów HTTP:

Zbieramy logi z Loki

Standardowe źródło danych Loki jest nieco ograniczone funkcjonalnie w porównaniu do źródła danych Prometheus (na przykład, nie można zmienić legendy), ale Loki można podłączyć jako źródło o typie Prometheus. Nie jestem pewien, czy jest to dokumentowane zachowanie, ale, sądząc po odpowiedzi deweloperów “Jak skonfigurować Loki jako źródło danych Prometheus? · Issue #1222 · grafana/loki”, na przykład, jest to całkowicie dozwolone, a Loki jest w pełni zgodny z PromQL.

Dodajemy Loki jako źródło danych o typie Prometheus i dopisujemy URL /loki:

Zbieramy logi z Loki

I możemy tworzyć wykresy, tak jak w przypadku, gdybyśmy pracowali z metrykami z Prometheus:

Zbieramy logi z Loki

Myślę, że rozbieżność w funkcjonalności jest tymczasowa i deweloperzy w przyszłości to naprawią.

Zbieramy logi z Loki

Metryki

W Loki dostępna jest możliwość wydobywania metryk liczbowych z logów i wysyłania ich do Prometheus. Na przykład, w logu nginx znajduje się liczba bajtów w odpowiedzi, a także, przy pewnej modyfikacji standardowego formatu logów, czas w sekundach, który był potrzebny na odpowiedź. Te dane można wydobyć i wysłać do Prometheus.

Dodajemy jeszcze jedną sekcję do promtail.yml:

- match:
   selector: '{request_type="api"}'
   stages:
     - metrics:
         http_nginx_response_time:
           type: Histogram
           description: "czas odpowiedzi 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: "suma bajtów odpowiedzi"
           source: bytes_out
           config:
             action: add
         http_nginx_response_bytes_count:
           type: Counter
           description: "liczba bajtów odpowiedzi"
           source: bytes_out
           config:
             action: inc

Opcja pozwala na definiowanie i aktualizowanie metryk na podstawie danych z extracted map. Te metryki nie są przesyłane do Loki — pojawiają się w Promtail /metrics endpoint. Prometheus musi być skonfigurowany, aby otrzymywać dane uzyskane na tym etapie. W podanym przykładzie dla request_type="api" zbieramy metrykę-histogram. Z tym rodzajem metryk wygodnie jest uzyskać percentyle. Dla statyk i zdjęć zbieramy sumę bajtów oraz liczbę wierszy, w których otrzymaliśmy bajty, aby obliczyć wartość średnią.

Więcej informacji o metrykach przeczytasz tutaj.

Otwieramy port na Promtail:

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

Upewniamy się, że metryki z prefiksem promtail_custom się pojawiły:

Zbieramy logi z Loki

Konfigurujemy Prometheus. Dodajemy job promtail:

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

I rysujemy wykres:

Zbieramy logi z Loki

W ten sposób możemy dowiedzieć się na przykład o czterech najwolniejszych zapytaniach. Można również ustawić monitorowanie dla tych danych metrycznych.

Skalowanie

Loki może działać w trybie pojedynczym (single binary mode) lub w trybie rozproszonym (horizontally-scalable mode). W tym drugim przypadku może przechowywać dane w chmurze, z chunkami i indeksami przechowywanymi osobno. W wersji 1.5 wprowadzono możliwość przechowywania w jednym miejscu, ale na razie nie jest ona zalecana do użycia w produkcji.

Zbieramy logi z Loki

Chunki można przechowywać w magazynie zgodnym z S3, a do przechowywania indeksów można używać baz danych skalowalnych poziomo: Cassandra, BigTable lub DynamoDB. Inne części Loki — Distributory (do zapisu) i Querier (do zapytań) — są stateless i również skalują się poziomo.

Na konferencji DevOpsDays Vancouver 2019 jeden z uczestników Callum Styan ogłosił, że z Loki jego projekt ma petabajty logów z indeksem mniejszym niż 1% całkowitego rozmiaru: "Jak Loki koreluje metryki i logi — i oszczędza twoje pieniądze”.

Porównanie Loki i ELK

Rozmiar indeksu

Do testowania uzyskanego rozmiaru indeksu wziąłem logi z kontenera nginx, dla którego ustawiano Pipeline opisany powyżej. Plik z logami zawierał 406 624 wiersze o łącznej objętości 109 MB. Logi generowano przez godzinę, około 100 zapisów na sekundę.

Przykład dwóch wierszy z logu:

Zbieramy logi z Loki

Przy indeksowaniu ELK dało to rozmiar indeksu 30,3 MB:

Zbieramy logi z Loki

W przypadku Loki dało to około 128 kB indeksu i około 3,8 MB danych w chunkach. Warto zauważyć, że log został sztucznie wygenerowany i nie różnił się dużą różnorodnością danych. Prosty gzip na początkowym dokerskim logu JSON z danymi dał kompresję 95,4%, a biorąc pod uwagę, że do samego Loki wysyłany był tylko oczyszczony log nginx, to kompresja do 4 MB jest zrozumiała. Całkowita liczba unikalnych wartości dla etykiet Loki wyniosła 35, co wyjaśnia mały rozmiar indeksu. Dla ELK log również był oczyszczany. W ten sposób, Loki skompresował dane pierwotne o 96%, a ELK - o 70%.

Zużycie pamięci

Zbieramy logi z Loki

Porównując cały stos Prometheus i ELK, Loki „pożera” o kilka razy mniej. Oczywiście, usługa napisana w Go zużywa mniej, niż usługa napisana w Javie, a porównanie rozmiaru JVM Heap Elasticsearch i przydzielonej pamięci dla Loki jest niewłaściwe, jednak warto zauważyć, że Loki używa znacznie mniej pamięci. Jego przewaga w zakresie CPU nie jest tak oczywista, ale również występuje.

Szybkość

Loki szybciej „przetwarza” logi. Szybkość zależy od wielu czynników - jakie to logi, jak wyrafinowanie je parsujemy, sieć, dysk itd. - ale jest zdecydowanie wyższa niż w przypadku ELK (w moim teście - około dwa razy). Wyjaśnia to fakt, że Loki umieszcza znacznie mniej danych w indeksie i w związku z tym wydaje mniej czasu na indeksowanie. Sytuacja z szybkością wyszukiwania jest odwrotna: Loki zauważalnie spowalnia przy danych o wielkości przekraczającej kilka gigabajtów, podczas gdy ELK ma prędkość wyszukiwania niezależną od rozmiaru danych.

Wyszukiwanie w logach

Loki znacznie ustępuje ELK pod względem możliwości wyszukiwania w logach. Grep z wyrażeniami regularnymi to potężne narzędzie, ale ustępuje dojrzałej bazie danych. Brak zapytań typu range, agregacja tylko po etykietach, niemożność wyszukiwania bez etykiet - wszystkie te ograniczenia utrudniają nam znajdowanie interesujących informacji w Loki. Nie oznacza to, że za pomocą Loki nie można nic znaleźć, ale określa sposób pracy z logami, kiedy najpierw znajdujesz problem na wykresach Prometheus, a następnie po tych etykietach szukasz, co się wydarzyło w logach.

Interfejs

Po pierwsze, to ładne (przepraszam, nie mogłem się powstrzymać). Grafana ma przyjemny w wyglądzie interfejs, ale Kibana jest znacznie bardziej funkcjonalna.

Zalety i wady Loki

Z zalet można wymienić to, że Loki integruje się z Prometheusem, co oznacza, że metryki i alerty otrzymujemy bez zbędnych konfiguracji. Jest wygodny do zbierania logów i ich przechowywania z Kubernetes Pods, ponieważ dziedziczy po Prometheusie możliwość odkrywania usług i automatycznie dodaje etykiety.

Do wad należy słaba dokumentacja. Niektóre rzeczy, takie jak cechy i możliwości Promtail, odkryłem dopiero w trakcie przeglądania kodu, na szczęście open-source. Kolejnym minusem są ograniczone możliwości parsowania. Na przykład, Loki nie potrafi parsować logów wieloliniowych. Warto również dodać, że Loki to stosunkowo młoda technologia (wydanie 1.0 miało miejsce w listopadzie 2019 roku).

Podsumowanie

Loki to w 100% interesująca technologia, która nadaje się do małych i średnich projektów, umożliwiając rozwiązywanie wielu problemów związanych z agregacją logów, przeszukiwaniem logów, monitorowaniem i analizą logów.

Nie używamy Loki w Badoo, ponieważ mamy stos ELK, który nam odpowiada i przez lata zyskał różne niestandardowe rozwiązania. Kluczową kwestią dla nas jest przeszukiwanie logów. Mając prawie 100 GB logów dziennie, ważne jest, aby móc wszystko znaleźć i to jeszcze szybciej. Do budowy wykresów i monitorowania wykorzystujemy inne rozwiązania, które są dostosowane do naszych potrzeb i zintegrowane ze sobą. Stos Loki ma wyraźne zalety, ale nie zaoferuje nam więcej, niż już posiadamy, a jego korzyści z pewnością nie zrekompensują kosztów migracji.

I choć po badaniach stało się jasne, że nie możemy używać Loki, mamy nadzieję, że ten post pomoże Wam w podjęciu decyzji.

Repozytorium z kodem użytym w artykule znajduje się tutaj.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster