Аналітика логів 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. У нього досить багато функцій, але нас цікавлять дві: можливість автоматично використовувати фільтри по партиціях та преагрегації даних. Він використовує схему даних схема даних, написану на 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

Додати коментар або відгук