Analiza logów Nginx przy użyciu Amazon Athena i Cube.js

Zazwyczaj do monitorowania i analizowania działania Nginx wykorzystywane są produkty komercyjne lub gotowe alternatywy typu open source, takie jak Prometheus + Grafana. Jest to dobra opcja do monitorowania lub analiz w czasie rzeczywistym, ale niezbyt wygodna do analizy historycznej. W przypadku każdego popularnego zasobu ilość danych z dzienników nginx szybko rośnie, a do analizy dużej ilości danych logiczne jest użycie czegoś bardziej wyspecjalizowanego.

W tym artykule opowiem Ci, jak możesz skorzystać Athena analizować logi, na przykładzie Nginx, a ja pokażę, jak z tych danych złożyć analityczny dashboard, korzystając z open source'owego frameworku Cube.js. Oto pełna architektura rozwiązania:

Analiza logów Nginx przy użyciu Amazon Athena i Cube.js

TL:DR;
Link do gotowego dashboardu.

Aby zbierać informacje, których używamy Biegły, do przetworzenia - Wąż strażacki AWS Kinesis Data и Klej AWS, do przechowywania - AWS-a3. Za pomocą tego pakietu możesz przechowywać nie tylko logi nginx, ale także inne zdarzenia, a także logi innych usług. Możesz zastąpić niektóre części stosu podobnymi, na przykład możesz zapisywać logi do kinesis bezpośrednio z nginx, omijając fluentd, lub użyć do tego logstash.

Zbieranie logów Nginx

Domyślnie logi Nginx wyglądają mniej więcej 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 analizować, ale znacznie łatwiej jest poprawić konfigurację Nginx, aby generowała logi w 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

Do przechowywania logów użyjemy S3. Dzięki temu możesz przechowywać i analizować logi w jednym miejscu, ponieważ Athena może pracować z danymi bezpośrednio w S3. W dalszej części artykułu opowiem jak poprawnie dodawać i przetwarzać logi, ale najpierw potrzebujemy czystego wiadra w S3, w którym nic więcej nie będzie przechowywane. Warto wcześniej zastanowić się, w jakim regionie będziesz tworzyć segment, ponieważ usługa Athena nie jest dostępna we wszystkich regionach.

Tworzenie obwodu w konsoli Athena

Utwórzmy w Athenie tabelę dla logów. Jest potrzebny zarówno do pisania, jak i czytania, jeśli planujesz używać Kinesis Firehose. Otwórz konsolę Athena i utwórz tabelę:

Tworzenie tabeli SQL

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://<YOUR-S3-BUCKET>'
TBLPROPERTIES ('has_encrypted_data'='false');

Tworzenie strumienia węża strażackiego Kinesis

Kinesis Firehose zapisze dane otrzymane z Nginx do S3 w wybranym formacie, dzieląc je na katalogi w formacie RRRR/MM/DD/HH. Przyda się to podczas odczytywania danych. Można oczywiście pisać bezpośrednio do S3 z poziomu fluentd, ale w tym przypadku trzeba będzie napisać JSON, a to jest nieefektywne ze względu na duży rozmiar plików. Dodatkowo w przypadku korzystania z PrestoDB lub Atheny, JSON jest najwolniejszym formatem danych. Otwórz więc konsolę Kinesis Firehose, kliknij „Utwórz strumień dostaw”, wybierz „bezpośredni PUT” w polu „dostawa”:

Analiza logów Nginx przy użyciu Amazon Athena i Cube.js

W następnej zakładce wybierz „Konwersja formatu zapisu” - „Włączone” i wybierz „Apache ORC” jako format nagrywania. Według niektórych badań Owena O'Malleya, jest to optymalny format dla PrestoDB i Athena. Używamy tabeli, którą utworzyliśmy powyżej, jako schematu. Należy pamiętać, że w kinezie można określić dowolną lokalizację S3, używany jest jedynie schemat z tabeli. Jeśli jednak określisz inną lokalizację S3, nie będziesz mógł odczytać tych rekordów z tej tabeli.

Analiza logów Nginx przy użyciu Amazon Athena i Cube.js

Wybieramy S3 do przechowywania i wiadro, które utworzyliśmy wcześniej. Aws Glue Crawler, o którym opowiem później, nie może pracować z prefiksami w wiadrze S3, dlatego ważne jest, aby pozostawić go pustym.

Analiza logów Nginx przy użyciu Amazon Athena i Cube.js

Pozostałe opcje można zmieniać w zależności od obciążenia; ja zazwyczaj używam opcji domyślnych. Należy pamiętać, że kompresja S3 nie jest dostępna, ale ORC domyślnie używa kompresji natywnej.

Biegły

Teraz, gdy skonfigurowaliśmy przechowywanie i odbieranie dzienników, musimy skonfigurować wysyłanie. Użyjemy Biegły, ponieważ kocham Ruby, ale możesz użyć Logstash lub wysłać logi bezpośrednio do kinesis. Serwer Fluentd można uruchomić na kilka sposobów, opowiem o dokerze, bo jest prosty i wygodny.

Najpierw potrzebujemy pliku konfiguracyjnego fluent.conf. Utwórz go i dodaj źródło:

rodzaj Naprzód
Port 24224
powiąż 0.0.0.0

Teraz możesz uruchomić serwer Fluentd. Jeżeli potrzebujesz bardziej zaawansowanej konfiguracji przejdź do Centrum Dockera Istnieje szczegółowy przewodnik, w tym, jak złożyć obraz.

$ docker run 
  -d 
  -p 24224:24224 
  -p 24224:24224/udp 
  -v /data:/fluentd/log 
  -v <PATH-TO-FLUENT-CONF>:/fluentd/etc fluentd 
  -c /fluentd/etc/fluent.conf
  fluent/fluentd:stable

Ta konfiguracja używa ścieżki /fluentd/log do buforowania dzienników przed wysłaniem. Możesz się bez tego obejść, ale po ponownym uruchomieniu możesz stracić wszystko zapisane w pamięci podręcznej z katorżniczą pracą. Możesz także użyć dowolnego portu; 24224 jest domyślnym portem Fluentd.

Teraz, gdy mamy uruchomiony Fluentd, możemy wysyłać tam logi Nginx. Zwykle uruchamiamy Nginx w kontenerze Dockera, w takim przypadku Docker ma natywny sterownik rejestrowania dla Fluentd:

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

Jeśli uruchomisz Nginx w inny sposób, możesz użyć plików dziennika, tak jak Fluentd wtyczka ogona pliku.

Dodajmy analizę logów skonfigurowaną powyżej do konfiguracji Fluent:

<filter YOUR-NGINX-TAG.*>
  @type parser
  key_name log
  emit_invalid_record_to_error false
  <parse>
    @type json
  </parse>
</filter>

I wysyłanie logów do Kinesis za pomocą wtyczka kinesis firehose:

<match YOUR-NGINX-TAG.*>
    @type kinesis_firehose
    region region
    delivery_stream_name <YOUR-KINESIS-STREAM-NAME>
    aws_key_id <YOUR-AWS-KEY-ID>
    aws_sec_key <YOUR_AWS-SEC_KEY>
</match>

Athena

Jeżeli wszystko skonfigurowałeś poprawnie, to po chwili (domyślnie Kinesis rejestruje otrzymane dane co 10 minut) powinieneś zobaczyć pliki logów w S3. W menu „monitorowanie” Kinesis Firehose możesz zobaczyć, ile danych jest zarejestrowanych w S3, a także błędy. Nie zapomnij dać dostępu do zapisu w segmencie S3 roli Kinesis. Jeśli Kinesis nie mógł czegoś przeanalizować, doda błędy do tego samego segmentu.

Teraz możesz przeglądać dane w Athenie. Znajdźmy najnowsze żądania, dla których zwróciliśmy 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 żądania

Teraz nasze logi zostały przetworzone i zapisane w S3 w ORC, skompresowane i gotowe do analizy. Kinesis Firehose zorganizował je nawet w katalogi na każdą godzinę. Jednakże, dopóki tabela nie jest podzielona na partycje, Athena będzie ładować dane wszechczasów na każde żądanie, z rzadkimi wyjątkami. Jest to duży problem z dwóch powodów:

  • Ilość danych stale rośnie, spowalniając zapytania;
  • Opłaty za usługę Athena są naliczane na podstawie ilości zeskanowanych danych, przy czym każde żądanie wynosi co najmniej 10 MB.

Aby to naprawić, używamy AWS Glue Crawler, który przeszuka dane w S3 i zapisze informacje o partycji w Glue Metastore. Umożliwi nam to użycie partycji jako filtra podczas wysyłania zapytań do Atheny i przeskanowanie tylko katalogów określonych w zapytaniu.

Konfigurowanie robota Amazon Glue Crawler

Amazon Glue Crawler skanuje wszystkie dane w zasobniku S3 i tworzy tabele z partycjami. Utwórz moduł Glue Crawler z konsoli AWS Glue i dodaj wiadro, w którym przechowujesz dane. Możesz użyć jednego przeszukiwacza do kilku zasobników. W takim przypadku utworzy on w określonej bazie danych tabele o nazwach pasujących do nazw zasobników. Jeśli planujesz regularnie korzystać z tych danych, pamiętaj o skonfigurowaniu harmonogramu uruchamiania Crawlera tak, aby odpowiadał Twoim potrzebom. Używamy jednego robota indeksującego dla wszystkich tabel, który działa co godzinę.

Podzielone tabele

Po pierwszym uruchomieniu przeszukiwacza tabele dla każdego przeskanowanego zasobnika powinny pojawić się w bazie danych określonej w ustawieniach. 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 6:7 a 8:2019 XNUMX kwietnia XNUMX roku. Ale o ile jest to bardziej wydajne niż zwykłe czytanie z tabeli nie podzielonej na partycje? Znajdźmy i wybierzmy te same rekordy, filtrując je według sygnatury czasowej:

Analiza logów Nginx przy użyciu Amazon Athena i Cube.js

3.59 sekundy i 244.34 megabajtów danych w zestawie danych obejmującym tylko tydzień dzienników. Spróbujmy filtrować według partycji:

Analiza logów Nginx przy użyciu Amazon Athena i Cube.js

Trochę szybciej, ale co najważniejsze – tylko 1.23 megabajta danych! Byłoby znacznie taniej, gdyby nie minimalne 10 megabajtów na żądanie w cenie. Ale nadal jest znacznie lepiej, a na dużych zbiorach danych różnica będzie znacznie bardziej imponująca.

Budowa dashboardu przy użyciu Cube.js

Do montażu dashboardu wykorzystujemy framework analityczny Cube.js. Posiada całkiem sporo funkcji, ale nas interesują dwie: możliwość automatycznego korzystania z filtrów partycji oraz wstępna agregacja danych. Wykorzystuje schemat danych schemat danych, napisany w JavaScript w celu wygenerowania kodu SQL i wykonania zapytania do bazy danych. Musimy jedynie wskazać, jak zastosować filtr partycji w schemacie danych.

Stwórzmy nową aplikację Cube.js. Ponieważ używamy już stosu AWS, logiczne jest użycie Lambdy do wdrożenia. Możesz użyć szablonu ekspresowego do generowania, jeśli planujesz hostować backend Cube.js w Heroku lub Dockerze. Dokumentacja opisuje inne metody hostingu.

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

Zmienne środowiskowe służą do konfigurowania dostępu do bazy danych w kostce.js. Generator utworzy plik .env, w którym możesz określić swoje klucze Athena.

Teraz potrzebujemy schemat danych, w którym wskażemy dokładnie, w jaki sposób przechowywane są nasze logi. Można tam również określić sposób obliczania metryk dla dashboardów.

W katalogu schema, utwórz plik Logs.js. Oto przykładowy model 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') AND
    date(from_iso8601_timestamp(${to})) >= 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: `Yes`
        }],
        else: { label: `No` }
      }
    },

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

Tutaj używamy zmiennej FILTR_PARAMYdo wygenerowania zapytania SQL z filtrem partycji.

Ustalamy także metryki i parametry, które chcemy wyświetlić na dashboardzie oraz określamy wstępne agregacje. Cube.js utworzy dodatkowe tabele ze wstępnie zagregowanymi danymi i automatycznie zaktualizuje dane po ich otrzymaniu. To nie tylko przyspiesza zapytania, ale także zmniejsza koszty korzystania z Atheny.

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 tym modelu określamy, że konieczne jest wstępne agregowanie danych dla wszystkich używanych metryk i stosowanie podziału na miesiące. Partycjonowanie przedagregacyjne może znacznie przyspieszyć gromadzenie i aktualizację danych.

Teraz możemy złożyć deskę rozdzielczą!

Backend Cube.js zapewnia REST API oraz zestaw bibliotek klienckich dla popularnych frameworków front-endowych. Do zbudowania dashboardu użyjemy wersji klienta React. Cube.js dostarcza tylko dane, więc przydałaby nam się biblioteka wizualizacyjna - podoba mi się przerysowuje, ale możesz użyć dowolnego.

Serwer Cube.js akceptuje żądanie w formacie JSON, który określa wymagane metryki. Na przykład, aby obliczyć, ile błędów dziennie wygenerował Nginx, musisz wysłać następujące żądanie:

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

Zainstalujmy klienta Cube.js i bibliotekę komponentów React poprzez NPM:

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

Importujemy komponenty cubejs и QueryRendereraby pobrać dane i zebrać dashboard:

Kod panelu

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 (
    <QueryRenderer
      query={{
        measures: ['Logs.errorCount'],
        timeDimensions: [{
            dimension: 'Logs.createdAt',
            dateRange: ['2019-01-01', '2019-01-07'],
            granularity: 'day'
        }]
      }}
      cubejsApi={cubejsApi}
      render={({ resultSet }) => {
        if (!resultSet) {
          return 'Loading...';
        }

        return (
          <LineChart data={resultSet.rawData()}>
            <XAxis dataKey="Logs.createdAt"/>
            <YAxis/>
            <Line type="monotone" dataKey="Logs.errorCount" stroke="#8884d8"/>
          </LineChart>
        );
      }}
    />
  )
}

Źródła panelu kontrolnego są dostępne pod adresem piaskownica kodu.

Źródło: www.habr.com

Dodaj komentarz