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ć 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:

TL:DR;
.
Do zbierania informacji używamy , do przetwarzania — i , do przechowywania — . 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”:

Na następnej karcie wybieramy „Konwersja formatu rekordów” — „Włączona” i wybieramy „Apache ORC” jako format do zapisu. Zgodnie z badaniami niektórych , 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.

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.

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ć , 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:
forward
port 24224
bind 0.0.0.0
Teraz możemy uruchomić serwer Fluentd. Jeśli potrzebujesz bardziej zaawansowanej konfiguracji, na 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:stableTa 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
nginxJeśli uruchamiasz Nginx w inny sposób, możesz używać plików logów, w Fluentd znajduje się .
Dodajmy do konfiguracji Fluent parsing logów, skonfigurowany powyżej:
@type parser
key_name log
emit_invalid_record_to_error false
@type jsonI wysyłanie logów do Kinesis, korzystając z :
@type kinesis_firehose
region region
delivery_stream_name
aws_key_id
aws_sec_keyAthena
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:

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

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 , 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 .
$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athenaDo 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 .
Teraz potrzebujemy , 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 , 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. może znacznie przyspieszyć zbieranie i aktualizację danych.
Teraz możemy stworzyć pulpit!
Backend Cube.js udostępnia 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ę , ale możesz użyć dowolnej.
Serwer Cube.js przyjmuje żądanie w , 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/reactImportujemy 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 .
Źródło: habr.com
