Звычайна для маніторынгу і аналізу працы Nginx выкарыстоўваюць камерцыйныя прадукты ці гатовыя open-source альтэрнатывы, такія як Prometheus + Grafana. Гэта добры варыянт для маніторынгу ці real-time аналітыкі, але не надта зручны для гістарычнага аналізу. На любым папулярным рэсурсе аб'ём дадзеных з логаў nginx хутка расце, і для аналізу вялікага аб'ёму дадзеных лагічна выкарыстоўваць нешта больш спецыялізаванае.
У гэтым артыкуле я раскажу, як можна выкарыстоўваць
TL:DR;
Для збору інфармацыі мы выкарыстоўваем
Збіраны логі Nginx
Па змаўчанні, логі Nginx выглядаюць неяк так:
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" "-"
Іх можна распарсіць, але значна прасцей паправіць канфігурацыю Nginx, каб ён выдаваў логі ў 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 для захоўвання
Каб захоўваць логі, мы будзем выкарыстоўваць S3. Гэта дазваляе захоўваць і аналізаваць логі ў адным месцы, бо Athena можа працаваць з дадзенымі ў S3 напрамую. Далей у артыкуле я раскажу, як правільна складаць і працэсіць логі, але для пачатку нам патрэбен чысты бакет у S3, у якім нічога больш захоўвацца не будзе. Варта загадзя падумаць, у якім рэгіёне вы створыце бакет, таму што Athena даступная не ва ўсіх рэгіёнах.
Ствараем схему ў кансолі Athena
Створым табліцу ў Athena для логаў. Яна патрэбна і для запісу, і для чытання, калі вы плануеце выкарыстоўваць Kinesis Firehose. Адкрываеце кансоль Athena і ствараеце табліцу:
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');
Ствараем Kinesis Firehose Stream
Kinesis Firehose запіша дадзеныя, атрыманыя ад Nginx, у S3 у абраным фармаце, разбіўшы па дырэкторыях у фармаце ГГГГ/ММ/ДД/ЧЧ. Гэта спатрэбіцца пры чытанні даных. Можна, вядома, пісаць напроста ў S3 з fluentd, але ў гэтым выпадку давядзецца пісаць JSON, а гэта неэфектыўна з-за вялікага памеру файлаў. Да таго ж, пры выкарыстанні PrestoDB або Athena, JSON - самы павольны фармат дадзеных. Так што адчыняны кансоль Kinesis Firehose, націсканы "Create delivery stream", выбіраемы "direct PUT" у поле "delivery":
У наступнай укладцы выбіраемы "Record format conversion" - "Enabled" і выбіраемы "Apache ORC" як фармат для запісу. Згодна з даследаваннямі некаторага
Выбіраемы S3 для захоўвання і бакет, які мы стварылі раней. Aws Glue Crawler, пра які я раскажу крыху пазней, не ўмее працаваць з прэфіксамі ў S3 бакеце, так што яго важна пакінуць пустым.
Астатнія опцыі можна змяняць у залежнасці ад вашай нагрузкі, я звычайна выкарыстоўваю дэфолтныя. Звярніце ўвагу, што сціск S3 недаступны, але ORC выкарыстоўвае ўласны сціск па змаўчанні.
Свабодна
Цяпер, калі ў нас настроена захоўванне і атрыманне логаў, трэба наладзіць адпраўку. Мы будзем выкарыстоўваць
Для пачатку, нам патрэбен файл канфігурацыі fluent.conf. Стварыце яго і дадайце source:
порт 24224
звязаць 0.0.0.0
Цяпер можна запусціць Fluentd сервер. Калі вам патрэбна больш прасунутая канфігурацыя, на
$ 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
Гэтая канфігурацыя выкарыстоўвае шлях /fluentd/log
для кэшавання логаў перад адпраўкай. Можна абыйсціся без гэтага, але тады пры перазапуску можна страціць усё закэшаванае непасільнай працай. Порт таксама можна выкарыстоўваць любы, 24224 – гэта дэфолтны порт Fluentd.
Цяпер, калі ў нас ёсць запушчаны Fluentd, мы можам адправіць туды логі Nginx. Мы звычайна запускаем Nginx у Docker-кантэйнеры, і ў гэтым выпадку ў Docker ёсць натыўны драйвер логаў для 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
Калі вы запускаеце Nginx інакш, вы можаце выкарыстоўваць лог-файлы, у Fluentd ёсць
Дадамо ў канфігурацыю Fluent парсінг логаў, наладжаны вышэй:
<filter YOUR-NGINX-TAG.*>
@type parser
key_name log
emit_invalid_record_to_error false
<parse>
@type json
</parse>
</filter>
І адпраўку логаў у Kinesis, выкарыстоўваючы
<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>
Афіна
Калі вы ўсё правільна наладзілі, то праз некаторы час (па-змаўчанні Kinesis запісвае атрыманыя дадзеныя раз у 10 хвілін) вы павінны ўбачыць файлы логаў у S3. У меню "monitoring" Kinesis Firehose можна ўбачыць, колькі дадзеных запісана ў S3, а гэтак жа памылкі. Не забудзьцеся даць доступ на запіс у бакет S3 для ролі Kinesis. Калі Kinesis нешта не змог распарсіць, ён складзе памылкі ў тым жа бакеце.
Цяпер можна паглядзець дадзеныя ў Athena. Давайце знойдзем свежыя запыты, на якія мы падалі памылкі:
SELECT * FROM "db_name"."table_name" WHERE status > 499 ORDER BY created_at DESC limit 10;
Сканіраванне ўсіх запісаў на кожны запыт
Цяпер нашы логі апрацаваны і складзены ў S3 у ORC, сціснутыя і гатовыя да аналізу. Kinesis Firehose нават расклаў іх па дырэкторыях на кожную гадзіну. Аднак, пакуль табліца не партыцыравана, Athena будзе загружаць дадзеныя за ўвесь час на кожны запыт, за рэдкім выключэннем. Гэта вялікая праблема па двух прычынах:
- Аб'ём дадзеных увесь час расце, запавольваючы запыты;
- Рахунак за Athena выстаўляецца ў залежнасці ад аб'ёму прасканаваных дадзеных, з мінімумам 10 МБ за кожны запыт.
Каб выправіць гэта, мы выкарыстоўваем AWS Glue Crawler, які праскануе дадзеныя ў S3 і запіша інфармацыю аб партыцыях у Glue Metastore. Гэта дазволіць нам выкарыстоўваць партіціі як фільтр пры запытах у Athena, і яна будзе сканаваць толькі дырэкторыі, указаныя ў запыце.
Наладжваем Amazon Glue Crawler
Amazon Glue Crawler скануе ўсе дадзеныя ў S3 бакеце і стварае табліцы з партыцыямі. Стварыце Glue Crawler з кансолі AWS Glue і дадайце бакет, у якім вы захоўваеце дадзеныя. Вы можаце выкарыстоўваць адзін краулер для некалькіх бакетаў, у гэтым выпадку ён створыць табліцы ў азначанай базе дадзеных з назвамі, якія супадаюць з назвамі бакетаў. Калі вы плануеце ўвесь час выкарыстоўваць гэтыя дадзеныя, не забудзьцеся наладзіць расклад запуску Crawler у адпаведнасці з вашымі патрэбамі. Мы выкарыстоўваем адзін Crawler для ўсіх табліц, які запускаецца кожную гадзіну.
Партыцыраваныя табліцы
Пасля першага запуску краўлера ў базе дадзеных, паказанай у наладах, павінны з'явіцца табліцы для кожнага прасканаванага бакета. Адкрыйце кансоль Athena і знайдзіце табліцу з логамі Nginx. Давайце паспрабуем што-небудзь прачытаць:
SELECT * FROM "default"."part_demo_kinesis_bucket"
WHERE(
partition_0 = '2019' AND
partition_1 = '04' AND
partition_2 = '08' AND
partition_3 = '06'
);
Гэты запыт абярэ ўсе запісы, атрыманыя з 6 да 7 раніцы 8 красавіка 2019 года. Але наколькі гэта больш эфектыўна, чым проста чытаць з не-партыкаванай табліцы? Давайце даведаемся і выберам тыя ж запісы, адфільтраваўшы іх па таймстэмпе:
3.59 секунды і 244.34 мегабайт дадзеных на датасеце, у якім усяго тыдзень логаў. Паспрабуем фільтр па партыцыях:
Ледзь хутчэй, але самае важнае – усяго 1.23 мегабайта дадзеных! Гэта было б значна танней, калі б не мінімальныя 10 мегабайт за запыт у прайсінг. Але ўсё роўна значна лепш, а на вялікіх датасетах розніца будзе куды больш уражлівай.
Збіраны дэшборд з дапамогай Cube.js
Каб сабраць дэшборд, мы выкарыстоўваем аналітычны фрэймворк Cube.js. У яго даволі шмат функцый, але нас цікавяць дзве: магчымасць аўтаматычна выкарыстоўваць фільтры па партыцыях і прэ-агрэгацыі дадзеных. Ён выкарыстоўвае схему дадзеных
Створым новае дадатак Cube.js. Так як мы ўжо выкарыстоўваем AWS-стэк, лагічна выкарыстоўваць Lambda для дэплою. Вы можаце выкарыстоўваць express-шаблон для генерацыі, калі плануеце хосціць Cube.js бэкэнд у Heroku ці Docker. У дакументацыі апісаны іншыя
$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athena
Для настройкі доступу да базы дадзеных у cube.js выкарыстоўваюцца зменныя асяроддзі. Генератар створыць файл .env, у якім вы можаце пазначыць вашыя ключы для
Цяпер нам спатрэбіцца
У дырэкторыі schema
, стварыце файл Logs.js
. Вось прыклад мадэлі дадзеных для nginx:
Код мадэлі
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`
}
}
});
Тут мы выкарыстоўваем зменную
Мы таксама задаем метрыкі і параметры, якія хочам адлюстраваць на дэшбордзе, і паказваем прэ-агрэгацыі. Cube.js створыць дадатковыя табліцы з преагрегированными дадзенымі і будзе аўтаматычна абнаўляць дадзеныя па меры паступлення. Гэта дазваляе не толькі паскорыць запыты, але і зменшыць кошт выкарыстання Athena.
Дадамо гэтую інфармацыю ў файл схемы дадзеных:
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`
)
}
}
}
Мы паказваем у гэтай мадэлі, што неабходна пре-агрэгаваць дадзеныя для ўсіх выкарыстоўваных метрык, і выкарыстоўваць партыцыраванне па месяцах.
Цяпер мы можам сабраць дэшборд!
Бэкенд Cube.js дае
Сервер Cube.js прымае запыт у
{
"measures": ["Logs.errorCount"],
"timeDimensions": [
{
"dimension": "Logs.createdAt",
"dateRange": ["2019-01-01", "2019-01-07"],
"granularity": "day"
}
]
}
Усталюем Cube.js кліент і бібліятэку React-кампанет праз NPM:
$ npm i --save @cubejs-client/core @cubejs-client/react
Імпартам кампанетны cubejs
и QueryRenderer
, каб выгрузіць дадзеныя, і збіраем дэшборд:
Код дэшборда
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>
);
}}
/>
)
}
Зыходнікі дэшборда даступныя на
Крыніца: habr.com