Аналітыка логаў Nginx з дапамогай Amazon Athena і Cube.js

Звычайна для маніторынгу і аналізу працы Nginx выкарыстоўваюць камерцыйныя прадукты ці гатовыя open-source альтэрнатывы, такія як Prometheus + Grafana. Гэта добры варыянт для маніторынгу ці real-time аналітыкі, але не надта зручны для гістарычнага аналізу. На любым папулярным рэсурсе аб'ём дадзеных з логаў nginx хутка расце, і для аналізу вялікага аб'ёму дадзеных лагічна выкарыстоўваць нешта больш спецыялізаванае.

У гэтым артыкуле я раскажу, як можна выкарыстоўваць Афіна для аналізу логаў, узяўшы для прыкладу Nginx, і пакажу, як з гэтых дадзеных сабраць аналітычны дэшборд, выкарыстоўваючы open-source фрэймворк cube.js. Вось поўная архітэктура рашэння:

Аналітыка логаў Nginx з дапамогай Amazon Athena і Cube.js

TL:DR;
Спасылка на гатовы дэшборд.

Для збору інфармацыі мы выкарыстоўваем Свабодна, для працэсінгу AWS Kinesis Data Firehose и Клей AWS, для захоўвання AWS S3. З дапамогай гэтай звязкі можна захоўваць не толькі логі nginx, але і іншыя эвенты, а таксама логі іншых сэрвісаў. Вы можаце замяніць некаторыя часткі на аналагічныя для вашага стэка, напрыклад, можна пісаць логі ў kinesis напрасткі з nginx, абыходзячы fluentd, або выкарыстоўваць logstash для гэтага.

Збіраны логі 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":

Аналітыка логаў Nginx з дапамогай Amazon Athena і Cube.js

У наступнай укладцы выбіраемы "Record format conversion" - "Enabled" і выбіраемы "Apache ORC" як фармат для запісу. Згодна з даследаваннямі некаторага Owen O'Malley, гэта аптымальны фармат для PrestoDB і Athena. У якасці схемы паказваем табліцу, якую мы стварылі вышэй. Звярніце ўвагу, што S3 location у kinesis можна паказаць любы, з табліцы выкарыстоўваецца толькі схема. Але калі вы пакажаце іншы S3 location, то прачытаць з гэтай табліцы гэтыя запісы не атрымаецца.

Аналітыка логаў Nginx з дапамогай Amazon Athena і Cube.js

Выбіраемы S3 для захоўвання і бакет, які мы стварылі раней. Aws Glue Crawler, пра які я раскажу крыху пазней, не ўмее працаваць з прэфіксамі ў S3 бакеце, так што яго важна пакінуць пустым.

Аналітыка логаў Nginx з дапамогай Amazon Athena і Cube.js

Астатнія опцыі можна змяняць у залежнасці ад вашай нагрузкі, я звычайна выкарыстоўваю дэфолтныя. Звярніце ўвагу, што сціск S3 недаступны, але ORC выкарыстоўвае ўласны сціск па змаўчанні.

Свабодна

Цяпер, калі ў нас настроена захоўванне і атрыманне логаў, трэба наладзіць адпраўку. Мы будзем выкарыстоўваць Свабодна, таму што я люблю Ruby, але вы можаце выкарыстоўваць Logstash ці адпраўляць логі ў kinesis напрамую. Fluentd сервер можна запусціць некалькімі спосабамі, я раскажу пра docker, таму што гэта проста і зручна.

Для пачатку, нам патрэбен файл канфігурацыі 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 ёсць file tail plugin.

Дадамо ў канфігурацыю Fluent парсінг логаў, наладжаны вышэй:

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

І адпраўку логаў у Kinesis, выкарыстоўваючы kinesis firehose plugin:

<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 года. Але наколькі гэта больш эфектыўна, чым проста чытаць з не-партыкаванай табліцы? Давайце даведаемся і выберам тыя ж запісы, адфільтраваўшы іх па таймстэмпе:

Аналітыка логаў Nginx з дапамогай Amazon Athena і Cube.js

3.59 секунды і 244.34 мегабайт дадзеных на датасеце, у якім усяго тыдзень логаў. Паспрабуем фільтр па партыцыях:

Аналітыка логаў Nginx з дапамогай Amazon Athena і Cube.js

Ледзь хутчэй, але самае важнае – усяго 1.23 мегабайта дадзеных! Гэта было б значна танней, калі б не мінімальныя 10 мегабайт за запыт у прайсінг. Але ўсё роўна значна лепш, а на вялікіх датасетах розніца будзе куды больш уражлівай.

Збіраны дэшборд з дапамогай Cube.js

Каб сабраць дэшборд, мы выкарыстоўваем аналітычны фрэймворк Cube.js. У яго даволі шмат функцый, але нас цікавяць дзве: магчымасць аўтаматычна выкарыстоўваць фільтры па партыцыях і прэ-агрэгацыі дадзеных. Ён выкарыстоўвае схему дадзеных data schema, напісаную на Javascript, каб згененраваць SQL і выканаць запыт да базы дадзеных. Ад нас патрабуецца толькі паказаць, як выкарыстоўваць фільтр па партыцыях у схеме дадзеных.

Створым новае дадатак 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`
    }
  }
});

Тут мы выкарыстоўваем зменную FILTER_PARAMS, Каб згенераваць SQL запыт з фільтрам па партыцыях.

Мы таксама задаем метрыкі і параметры, якія хочам адлюстраваць на дэшбордзе, і паказваем прэ-агрэгацыі. 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 дае REST API і набор кліенцкіх бібліятэк для папулярных фронтэнд-фрэймворкаў. Мы скарыстаемся React-версіяй кліента для зборкі дэшборда. Cube.js падае толькі дадзеныя, так што нам спатрэбіцца бібліятэка для візуалізацый – мне падабаецца recharts, Але вы можаце выкарыстоўваць любую.

Сервер Cube.js прымае запыт у JSON фармаце, у якім указаны неабходныя метрыкі. Напрыклад, каб палічыць, колькі памылак аддаў Nginx па днях, трэба даслаць такі запыт:

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

Дадаць каментар