Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Zwykle do monitorowania i analizy działania Nginx wykorzystuje się komercyjne produkty lub gotowe alternatywy open-source, takie jak Prometheus + Grafana. To dobry wybór do monitorowania lub analityki w czasie rzeczywistym, ale niewygodny do analizy historycznej. Na każdej popularnej stronie objętość danych z logów Nginx szybko rośnie, dlatego do analizy dużych zbiorów danych warto zastosować coś bardziej wyspecjalizowanego.

W tym artykule opowiem, jak można wykorzystać Athena do analizy logów, używając Nginx jako przykładu, i pokażę, jak z tych danych zbudować analityczny pulpit, wykorzystując framework open-source cube.js. Oto pełna architektura rozwiązania:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

TL:DR;
Link do gotowego pulpitu.

Do zbierania informacji używamy Fluentd, do przetwarzania — AWS Kinesis Data Firehose i AWS Glue, do przechowywania — AWS S3. Dzięki temu połączeniu można przechowywać nie tylko logi Nginx, ale również inne zdarzenia oraz logi z innych usług. Można zamienić niektóre elementy na odpowiednie dla swojego stosu, na przykład można zapisywać logi bezpośrednio w Kinesis z Nginx, omijając Fluentd, lub użyć Logstash do tego celu.

Zbieramy logi Nginx

Domyślnie logi Nginx wyglądają tak:

4/9/2019 12:58:17 PM1.1.1.1 - - [09/Apr/2019:09:58:17 +0000] "GET /sign-up HTTP/2.0" 200 9168 "https://example.com/sign-in" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"
4/9/2019 12:58:17 PM1.1.1.1 - - [09/Apr/2019:09:58:17 +0000] "GET /sign-in HTTP/2.0" 200 9168 "https://example.com/sign-up" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"

Można je sparsować, ale znacznie łatwiej jest dostosować konfigurację Nginx, aby generował logi w formacie JSON:

log_format json_combined escape=json '{ "created_at": "$msec", '
            '"remote_addr": "$remote_addr", '
            '"remote_user": "$remote_user", '
            '"request": "$request", '
            '"status": $status, '
            '"bytes_sent": $bytes_sent, '
            '"request_length": $request_length, '
            '"request_time": $request_time, '
            '"http_referrer": "$http_referer", '
            '"http_x_forwarded_for": "$http_x_forwarded_for", '
            '"http_user_agent": "$http_user_agent" }';

access_log  /var/log/nginx/access.log  json_combined;

S3 do przechowywania

Aby przechowywać logi, będziemy używać S3. Umożliwia to przechowywanie i analizowanie logów w jednym miejscu, ponieważ Athena może bezpośrednio pracować z danymi w S3. W dalszej części artykułu opowiem, jak prawidłowo przechowywać i przetwarzać logi, ale najpierw potrzebujemy czystego koszyka w S3, w którym nic innego nie będzie przechowywane. Warto wcześniej pomyśleć, w jakim regionie utworzycie koszyk, ponieważ Athena nie jest dostępna we wszystkich regionach.

Tworzymy schemę w konsoli Athena

Stworzymy tabelę w Athena dla logów. Jest ona potrzebna zarówno do zapisu, jak i do odczytu, jeśli planujecie używać Kinesis Firehose. Otwieracie konsolę Athena i tworzycie tabelę:

SQL tworzenia tabeli

CREATE EXTERNAL TABLE `kinesis_logs_nginx`(
  `created_at` double, 
  `remote_addr` string, 
  `remote_user` string, 
  `request` string, 
  `status` int, 
  `bytes_sent` int, 
  `request_length` int, 
  `request_time` double, 
  `http_referrer` string, 
  `http_x_forwarded_for` string, 
  `http_user_agent` string)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.ql.io.orc.OrcSerde' 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.orc.OrcInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat'
LOCATION
  's3://'
TBLPROPERTIES ('has_encrypted_data'='false');

Tworzymy strumień Kinesis Firehose

Kinesis Firehose zapisze dane uzyskane z Nginx w S3 w wybranym formacie, rozdzielając się po katalogach w formacie RRRR/MM/DD/GG. Będzie to przydatne przy odczycie danych. Można oczywiście pisać bezpośrednio do S3 z fluentd, ale w takim przypadku trzeba będzie pisać JSON, co jest nieefektywne z powodu dużych rozmiarów plików. Ponadto, przy użyciu PrestoDB lub Athena, JSON jest najwolniejszym formatem danych. Otwieramy więc konsolę Kinesis Firehose, klikamy „Utwórz strumień dostawy”, wybieramy „direct PUT” w polu „dostawa”:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Na następnej karcie wybieramy „Konwersja formatu rekordów” — „Włączona” i wybieramy „Apache ORC” jako format do zapisu. Zgodnie z badaniami niektórych Owen O’Malley, to optymalny format dla PrestoDB i Athena. Jako schemat podajemy tabelę, którą stworzyliśmy powyżej. Zauważcie, że lokalizacja S3 w Kinesis może być dowolna, z tabeli używana jest tylko schemat. Ale jeśli wskażecie inną lokalizację S3, to nie będziecie mogli odczytać tych zapisów z tej tabeli.

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Wybieramy S3 do przechowywania i koszyk, który stworzyliśmy wcześniej. Aws Glue Crawler, o którym opowiem za chwilę, nie potrafi pracować z prefiksami w koszyku S3, więc ważne jest, aby go pozostawić pustym.

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Inne opcje można dostosować w zależności od obciążenia, zwykle korzystam z domyślnych. Zwróć uwagę, że kompresja S3 nie jest dostępna, ale ORC używa swojej własnej kompresji domyślnie.

Fluentd

Teraz, gdy mamy skonfigurowane przechowywanie i odbieranie logów, musimy skonfigurować ich wysyłkę. Będziemy używać Fluentd, ponieważ lubię Ruby, ale możesz używać Logstash lub bezpośrednio wysyłać logi do kinesis. Serwer Fluentd można uruchomić na kilka sposobów, opowiem o dockerze, ponieważ jest to proste i wygodne.

Na początek potrzebujemy pliku konfiguracyjnego fluent.conf. Utwórz go i dodaj źródło:

type forward
port 24224
bind 0.0.0.0

Teraz możemy uruchomić serwer Fluentd. Jeśli potrzebujesz bardziej zaawansowanej konfiguracji, na Docker Hub jest szczegółowy przewodnik, w tym o tym, jak zbudować własny obraz.

$ docker run 
  -d 
  -p 24224:24224 
  -p 24224:24224/udp 
  -v /data:/fluentd/log 
  -v :/fluentd/etc fluentd 
  -c /fluentd/etc/fluent.conf
  fluent/fluentd:stable

Ta konfiguracja używa ścieżki /fluentd/log do cache'owania logów przed wysyłką. Możesz się bez tego obejść, ale wtedy przy ponownym uruchomieniu możesz stracić wszystkie zapisane trudami dane. Port również można używać dowolnie, 24224 to domyślny port Fluentd.

Teraz, gdy mamy uruchomiony Fluentd, możemy wysyłać logi Nginx. Zwykle uruchamiamy Nginx w kontenerze Docker, a w tym przypadku Docker ma natywny sterownik logów dla Fluentd:

$ docker run 
--log-driver=fluentd 
--log-opt fluentd-address=
--log-opt tag="{{.Name}}" 
-v /some/content:/usr/share/nginx/html:ro 
-d 
nginx

Jeśli uruchamiasz Nginx w inny sposób, możesz używać plików logów, w Fluentd znajduje się file tail plugin.

Dodajmy do konfiguracji Fluent parsing logów, skonfigurowany powyżej:

@type parser
  key_name log
  emit_invalid_record_to_error false
  
    @type json

I wysyłanie logów do Kinesis, korzystając z kinesis firehose plugin:

@type kinesis_firehose
    region region
    delivery_stream_name 
    aws_key_id 
    aws_sec_key

Athena

Jeśli skonfigurowałeś wszystko poprawnie, to w ciągu pewnego czasu (domyślnie Kinesis zapisuje odebrane dane co 10 minut) powinieneś zobaczyć pliki logów w S3. W menu „monitoring” Kinesis Firehose można zobaczyć, ile danych zapisano w S3, a także błędy. Nie zapomnij nadać dostępu do zapisu do wiadra S3 dla roli Kinesis. Jeśli Kinesis nie może czegoś sparsować, zapisze błędy w tym samym wiadrze.

Teraz możemy zobaczyć dane w Athena. Znajdźmy świeże zapytania, w których wystąpiły błędy:

SELECT * FROM "db_name"."table_name" WHERE status > 499 ORDER BY created_at DESC LIMIT 10;

Skanowanie wszystkich rekordów dla każdego zapytania

Nasze logi zostały teraz przetworzone i zapisane w S3 w formacie ORC, skompresowane i gotowe do analizy. Kinesis Firehose nawet uporządkował je w katalogach na każdą godzinę. Jednakże, dopóki tabela nie jest partycjonowana, Athena będzie ładować dane z całego okresu dla każdego zapytania, z wyjątkiem rzadkich przypadków. To jest duży problem z dwóch powodów:

  • Objętość danych stale rośnie, co spowalnia zapytania;
  • Koszt za Athena jest naliczany w zależności od objętości skanowanych danych, z minimalną wartością 10 MB za każde zapytanie.

Aby to naprawić, używamy AWS Glue Crawler, który przeskanuje dane w S3 i zapisze informacje o partycjach w Glue Metastore. Dzięki temu będziemy mogli używać partycji jako filtru podczas zapytań w Athena, a ona będzie skanować tylko katalogi podane w zapytaniu.

Konfiguracja Amazon Glue Crawler

Amazon Glue Crawler skanuje wszystkie dane w koszyku S3 i tworzy tabele z partycjami. Utwórz Glue Crawler z konsoli AWS Glue i dodaj koszyk, w którym przechowujesz dane. Możesz użyć jednego crawlera dla wielu koszyków, w takim przypadku utworzy tabele w określonej bazie danych o nazwach odpowiadających nazwom koszyków. Jeśli planujesz regularnie korzystać z tych danych, nie zapomnij skonfigurować harmonogramu uruchamiania Crawlera zgodnie z własnymi potrzebami. Używamy jednego Crawlera dla wszystkich tabel, który uruchamia się co godzinę.

Tabele partycjonowane

Po pierwszym uruchomieniu crawlera w bazie danych określonej w ustawieniach, powinny pojawić się tabele dla każdego przeskanowanego koszyka. Otwórz konsolę Athena i znajdź tabelę z logami Nginx. Spróbujmy coś przeczytać:

SELECT * FROM "default"."part_demo_kinesis_bucket"
WHERE(
  partition_0 = '2019' AND
  partition_1 = '04' AND
  partition_2 = '08' AND
  partition_3 = '06'
  );

To zapytanie wybierze wszystkie rekordy otrzymane między godziną 6 a 7 rano 8 kwietnia 2019 roku. Ale jak bardzo to jest wydajniejsze niż po prostu czytanie z tabeli nie-partycjonowanej? Sprawdźmy to i wybierzmy te same rekordy, filtrując je według znaczników czasowych:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

3.59 sekundy i 244.34 megabajtów danych w zbiorze, który zawiera tylko tydzień logów. Spróbujemy filtrować według partycji:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Trochę szybciej, ale najważniejsze — tylko 1,23 megabajta danych! To byłoby znacznie tańsze, gdyby nie minimalne 10 megabajtów za zapytanie w cenniku. Jednak wciąż znacznie lepiej, a w przypadku dużych zbiorów danych różnica będzie jeszcze bardziej imponująca.

Собираем дэшборд с помощью Cube.js

Aby stworzyć pulpit, używamy analitycznego frameworka Cube.js. Ma on całkiem sporo funkcji, ale interesują nas dwie: możliwość automatycznego stosowania filtrów według partycji i pre-agregacji danych. Używa schemy danych data schema, napisanej w JavaScript, aby wygenerować SQL i wykonać zapytanie do bazy danych. Od nas wymaga się jedynie wskazania, jak zastosować filtr według partycji w schemie danych.

Stworzymy nową aplikację Cube.js. Ponieważ już korzystamy ze stosu AWS, logiczne jest używanie Lambda do wdrożenia. Możesz wykorzystać szablon express do generacji, jeśli planujesz hostować backend Cube.js w Heroku lub Dockerze. W dokumentacji opisano inne metody hostingu.

$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athena

Do skonfigurowania dostępu do bazy danych w cube.js używane są zmienne środowiskowe. Generator utworzy plik .env, w którym możesz podać swoje klucze dla Athena.

Teraz potrzebujemy schemy danych, w której określimy, jak dokładnie przechowywane są nasze logi. Można tam również określić, jak obliczać metryki dla pulpitów.

W katalogu schema, utwórz plik Logs.js. Oto przykład modelu danych dla nginx:

Kod modelu

const partitionFilter = (from, to) => `
    date(from_iso8601_timestamp(${from})) = date_parse(partition_0 || partition_1 || partition_2, '%Y%m%d')
    `

cube(`Logs`, {
  sql: `
  select * from part_demo_kinesis_bucket
  WHERE ${FILTER_PARAMS.Logs.createdAt.filter(partitionFilter)}
  `,

  measures: {
    count: {
      type: `count`,
    },

    errorCount: {
      type: `count`,
      filters: [
        { sql: `${CUBE.isError} = 'Yes'` }
      ]
    },

    errorRate: {
      type: `number`,
      sql: `100.0 * ${errorCount} / ${count}`,
      format: `percent`
    }
  },

  dimensions: {
    status: {
      sql: `status`,
      type: `number`
    },

    isError: {
      type: `string`,
      case: {
        when: [{
          sql: `${CUBE}.status >= 400`, label: `Tak`
        }],
        else: { label: `Nie` }
      }
    },

    createdAt: {
      sql: `from_unixtime(created_at)`,
      type: `time`
    }
  }
});

Tutaj używamy zmiennej FILTER_PARAMS, aby wygenerować zapytanie SQL z filtrem według partycji.

Definiujemy również metryki i parametry, które chcemy wyświetlić na pulpicie, oraz wskazujemy pre-agregacje. Cube.js stworzy dodatkowe tabele z pre-agregowanymi danymi i będzie automatycznie aktualizować dane w miarę ich napływu. Pozwala to nie tylko na przyspieszenie zapytań, ale także na zmniejszenie kosztów korzystania z Athena.

Dodajmy te informacje do pliku schematu danych:

preAggregations: {
  main: {
    type: `rollup`,
    measureReferences: [count, errorCount],
    dimensionReferences: [isError, status],
    timeDimensionReference: createdAt,
    granularity: `day`,
    partitionGranularity: `month`,
    refreshKey: {
      sql: FILTER_PARAMS.Logs.createdAt.filter((from, to) => 
        `select
           CASE WHEN from_iso8601_timestamp(${to}) + interval '3' day > now()
           THEN date_trunc('hour', now()) END`
      )
    }
  }
}

W tej modelu wskazujemy, że należy pre-agregować dane dla wszystkich używanych metryk i stosować partycjonowanie według miesięcy. Partyjonowanie pre-agregacji może znacznie przyspieszyć zbieranie i aktualizację danych.

Teraz możemy stworzyć pulpit!

Backend Cube.js udostępnia REST API i zestaw bibliotek klienckich dla popularnych frameworków frontendowych. Skorzystamy z wersji klienta React do zbudowania pulpitu. Cube.js dostarcza jedynie dane, dlatego będziemy potrzebować biblioteki do wizualizacji — podoba mi się recharts, ale możesz użyć dowolnej.

Serwer Cube.js przyjmuje żądanie w formacie JSON, które określa potrzebne metryki. Na przykład, aby policzyć, ile błędów zgłosił Nginx w ciągu dni, musisz wysłać takie żądanie:

{
  "measures": ["Logs.errorCount"],
  "timeDimensions": [
    {
      "dimension": "Logs.createdAt",
      "dateRange": ["2019-01-01", "2019-01-07"],
      "granularity": "day"
    }
  ]
}

Zainstalujmy klienta Cube.js i bibliotekę komponentu React za pomocą NPM:

$ npm i --save @cubejs-client/core @cubejs-client/react

Importujemy komponenty cubejs i QueryRenderer, aby pobrać dane, i tworzymy pulpit:

Kod pulpitu

import React from 'react';
import { LineChart, Line, XAxis, YAxis } from 'recharts';
import cubejs from '@cubejs-client/core';
import { QueryRenderer } from '@cubejs-client/react';

const cubejsApi = cubejs(
  'YOUR-CUBEJS-API-TOKEN',
  { apiUrl: 'http://localhost:4000/cubejs-api/v1' },
);

export default () => {
  return (
     {
        if (!resultSet) {
          return 'Ładowanie...';
        }

        return (
          
            
            
            
          
        );
      }}
    />
  )
}

Źródła kokpitu są dostępne pod CodeSandbox.

Ź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