Nginx log analytics gamit ang Amazon Athena at Cube.js

Karaniwan, ang mga komersyal na produkto o mga handa na open-source na alternatibo, tulad ng Prometheus + Grafana, ay ginagamit upang subaybayan at suriin ang pagpapatakbo ng Nginx. Ito ay isang magandang opsyon para sa pagsubaybay o real-time na analytics, ngunit hindi masyadong maginhawa para sa makasaysayang pagsusuri. Sa anumang tanyag na mapagkukunan, ang dami ng data mula sa mga log ng nginx ay mabilis na lumalaki, at upang pag-aralan ang isang malaking halaga ng data, lohikal na gumamit ng isang bagay na mas dalubhasa.

Sa artikulong ito sasabihin ko sa iyo kung paano mo magagamit Atenas upang suriin ang mga log, kunin ang Nginx bilang isang halimbawa, at ipapakita ko kung paano mag-assemble ng analytical dashboard mula sa data na ito gamit ang open-source na cube.js framework. Narito ang kumpletong arkitektura ng solusyon:

Nginx log analytics gamit ang Amazon Athena at Cube.js

TL:DR;
Mag-link sa natapos na dashboard.

Upang mangolekta ng impormasyong ginagamit namin Fluentd, para sa pagpoproseso - AWS Kinesis Data Firehose ΠΈ AWS Pandikit, para sa imbakan - AWS S3. Gamit ang bundle na ito, maaari kang mag-imbak hindi lamang ng mga log ng nginx, kundi pati na rin ang iba pang mga kaganapan, pati na rin ang mga log ng iba pang mga serbisyo. Maaari mong palitan ang ilang bahagi ng mga katulad na bahagi para sa iyong stack, halimbawa, maaari kang sumulat ng mga log sa kinesis nang direkta mula sa nginx, pag-bypass ng fluentd, o gumamit ng logstash para dito.

Pagkolekta ng mga log ng Nginx

Bilang default, ganito ang hitsura ng mga log ng 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" "-"

Maaari silang mai-parse, ngunit mas madaling itama ang pagsasaayos ng Nginx upang makagawa ito ng mga log sa 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 para sa imbakan

Upang mag-imbak ng mga log, gagamitin namin ang S3. Nagbibigay-daan ito sa iyo na mag-imbak at magsuri ng mga log sa isang lugar, dahil maaaring direktang gumana si Athena sa data sa S3. Sa ibang pagkakataon sa artikulo sasabihin ko sa iyo kung paano idagdag at iproseso nang tama ang mga log, ngunit kailangan muna namin ng malinis na bucket sa S3, kung saan wala nang iba pang maiimbak. Ito ay nagkakahalaga ng pag-iisip nang maaga kung saang rehiyon ka lilikha ng iyong bucket, dahil hindi available ang Athena sa lahat ng rehiyon.

Paglikha ng isang circuit sa Athena console

Gumawa tayo ng table sa Athena para sa mga log. Ito ay kinakailangan para sa parehong pagsusulat at pagbabasa kung plano mong gamitin ang Kinesis Firehose. Buksan ang Athena console at lumikha ng isang talahanayan:

Paglikha ng talahanayan ng 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');

Paglikha ng Kinesis Firehose Stream

Isusulat ng Kinesis Firehose ang data na natanggap mula sa Nginx hanggang S3 sa napiling format, hahatiin ito sa mga direktoryo sa format na YYYY/MM/DD/HH. Ito ay magiging kapaki-pakinabang kapag nagbabasa ng data. Maaari mong, siyempre, sumulat nang direkta sa S3 mula sa fluentd, ngunit sa kasong ito kakailanganin mong isulat ang JSON, at ito ay hindi epektibo dahil sa malaking sukat ng mga file. Bukod pa rito, kapag gumagamit ng PrestoDB o Athena, ang JSON ang pinakamabagal na format ng data. Kaya buksan ang Kinesis Firehose console, i-click ang "Gumawa ng stream ng paghahatid", piliin ang "direct PUT" sa field na "delivery":

Nginx log analytics gamit ang Amazon Athena at Cube.js

Sa susunod na tab, piliin ang "Record format conversion" - "Enabled" at piliin ang "Apache ORC" bilang format ng pag-record. Ayon sa ilang pananaliksik Owen O'Malley, ito ang pinakamainam na format para sa PrestoDB at Athena. Ginagamit namin ang talahanayan na aming ginawa sa itaas bilang isang schema. Pakitandaan na maaari mong tukuyin ang anumang lokasyon ng S3 sa kinesis; ang schema lamang ang ginagamit mula sa talahanayan. Ngunit kung tumukoy ka ng ibang lokasyon ng S3, hindi mo mababasa ang mga talang ito mula sa talahanayang ito.

Nginx log analytics gamit ang Amazon Athena at Cube.js

Pinipili namin ang S3 para sa imbakan at ang bucket na ginawa namin kanina. Ang Aws Glue Crawler, na tatalakayin ko sa ibang pagkakataon, ay hindi maaaring gumana sa mga prefix sa isang S3 bucket, kaya mahalagang iwanan itong walang laman.

Nginx log analytics gamit ang Amazon Athena at Cube.js

Ang natitirang mga opsyon ay maaaring baguhin depende sa iyong load; karaniwan kong ginagamit ang mga default. Tandaan na hindi available ang S3 compression, ngunit gumagamit ang ORC ng native compression bilang default.

Fluentd

Ngayong na-configure na namin ang pag-iimbak at pagtanggap ng mga log, kailangan naming i-configure ang pagpapadala. Gagamitin natin Fluentd, dahil mahal ko si Ruby, ngunit maaari mong gamitin ang Logstash o direktang magpadala ng mga log sa kinesis. Ang Fluentd server ay maaaring ilunsad sa maraming paraan, sasabihin ko sa iyo ang tungkol sa docker dahil ito ay simple at maginhawa.

Una, kailangan namin ang fluent.conf configuration file. Gawin ito at magdagdag ng pinagmulan:

uri pasulong
port 24224
magbigkis 0.0.0.0

Ngayon ay maaari mong simulan ang Fluentd server. Kung kailangan mo ng mas advanced na configuration, pumunta sa Docker hub Mayroong detalyadong gabay, kabilang ang kung paano i-assemble ang iyong larawan.

$ 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

Ginagamit ng configuration na ito ang path /fluentd/log sa cache logs bago ipadala. Magagawa mo nang wala ito, ngunit pagkatapos ay kapag nag-restart ka, maaari mong mawala ang lahat ng naka-cache na may back-breaking labor. Maaari mo ring gamitin ang anumang port; ang 24224 ay ang default na Fluentd port.

Ngayon na mayroon kaming Fluentd na tumatakbo, maaari kaming magpadala ng mga log ng Nginx doon. Karaniwan naming pinapatakbo ang Nginx sa isang lalagyan ng Docker, kung saan ang Docker ay may katutubong driver ng pag-log para sa 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

Kung iba ang pagpapatakbo mo ng Nginx, maaari kang gumamit ng mga log file, mayroon ang Fluentd file tail plugin.

Idagdag natin ang log parsing na na-configure sa itaas sa Fluent configuration:

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

At pagpapadala ng mga log sa Kinesis gamit 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>

Atenas

Kung na-configure mo nang tama ang lahat, pagkatapos ng ilang sandali (bilang default, ang mga tala ng Kinesis ay nakatanggap ng data isang beses bawat 10 minuto) dapat mong makita ang mga log file sa S3. Sa menu na "monitoring" ng Kinesis Firehose makikita mo kung gaano karaming data ang naitala sa S3, pati na rin ang mga error. Huwag kalimutang magbigay ng access sa pagsulat sa S3 bucket sa papel na Kinesis. Kung hindi ma-parse ng Kinesis ang isang bagay, idaragdag nito ang mga error sa parehong bucket.

Ngayon ay maaari mong tingnan ang data sa Athena. Hanapin natin ang pinakabagong mga kahilingan kung saan nagbalik tayo ng mga error:

SELECT * FROM "db_name"."table_name" WHERE status > 499 ORDER BY created_at DESC limit 10;

Ini-scan ang lahat ng mga tala para sa bawat kahilingan

Ngayon ang aming mga log ay naproseso at nakaimbak sa S3 sa ORC, naka-compress at handa na para sa pagsusuri. Inayos pa nga ng Kinesis Firehose ang mga ito sa mga direktoryo para sa bawat oras. Gayunpaman, hangga't hindi nahahati ang talahanayan, maglo-load si Athena ng lahat ng oras na data sa bawat kahilingan, na may mga bihirang pagbubukod. Ito ay isang malaking problema sa dalawang kadahilanan:

  • Ang dami ng data ay patuloy na lumalaki, nagpapabagal sa mga query;
  • Sinisingil si Athena batay sa dami ng data na na-scan, na may minimum na 10 MB bawat kahilingan.

Upang ayusin ito, gumagamit kami ng AWS Glue Crawler, na magko-crawl sa data sa S3 at magsusulat ng impormasyon ng partition sa Glue Metastore. Ito ay magbibigay-daan sa amin na gumamit ng mga partisyon bilang isang filter kapag nagtatanong kay Athena, at i-scan lamang nito ang mga direktoryo na tinukoy sa query.

Pagse-set up ng Amazon Glue Crawler

Ini-scan ng Amazon Glue Crawler ang lahat ng data sa S3 bucket at gumagawa ng mga talahanayan na may mga partisyon. Gumawa ng Glue Crawler mula sa AWS Glue console at magdagdag ng bucket kung saan mo iniimbak ang data. Maaari kang gumamit ng isang crawler para sa ilang mga bucket, kung saan lilikha ito ng mga talahanayan sa tinukoy na database na may mga pangalan na tumutugma sa mga pangalan ng mga bucket. Kung plano mong gamitin ang data na ito nang regular, tiyaking i-configure ang iskedyul ng paglulunsad ng Crawler upang umangkop sa iyong mga pangangailangan. Gumagamit kami ng isang Crawler para sa lahat ng talahanayan, na tumatakbo bawat oras.

Nahati ang mga mesa

Pagkatapos ng unang paglunsad ng crawler, dapat na lumabas ang mga talahanayan para sa bawat na-scan na bucket sa database na tinukoy sa mga setting. Buksan ang Athena console at hanapin ang talahanayan na may mga log ng Nginx. Subukan nating magbasa ng isang bagay:

SELECT * FROM "default"."part_demo_kinesis_bucket"
WHERE(
  partition_0 = '2019' AND
  partition_1 = '04' AND
  partition_2 = '08' AND
  partition_3 = '06'
  );

Pipiliin ng query na ito ang lahat ng record na natanggap sa pagitan ng 6 a.m. at 7 a.m. sa Abril 8, 2019. Ngunit gaano ito mas mahusay kaysa sa pagbabasa lamang mula sa isang hindi nahati na talahanayan? Alamin natin at piliin ang parehong mga tala, sinasala ang mga ito ayon sa timestamp:

Nginx log analytics gamit ang Amazon Athena at Cube.js

3.59 segundo at 244.34 megabytes ng data sa isang dataset na may isang linggo lang ng mga log. Subukan natin ang isang filter sa pamamagitan ng partition:

Nginx log analytics gamit ang Amazon Athena at Cube.js

Medyo mas mabilis, ngunit ang pinakamahalaga - 1.23 megabytes lamang ng data! Ito ay magiging mas mura kung hindi para sa minimum na 10 megabytes bawat kahilingan sa pagpepresyo. Ngunit mas maganda pa rin ito, at sa malalaking dataset ang pagkakaiba ay magiging mas kahanga-hanga.

Pagbuo ng dashboard gamit ang Cube.js

Upang i-assemble ang dashboard, ginagamit namin ang Cube.js analytical framework. Mayroon itong napakaraming function, ngunit interesado kami sa dalawa: ang kakayahang awtomatikong gumamit ng mga filter ng partition at pre-aggregation ng data. Gumagamit ito ng data schema schema ng data, nakasulat sa Javascript upang makabuo ng SQL at magsagawa ng query sa database. Kailangan lang naming ipahiwatig kung paano gamitin ang filter ng partition sa schema ng data.

Gumawa tayo ng bagong Cube.js application. Dahil ginagamit na namin ang AWS stack, lohikal na gamitin ang Lambda para sa pag-deploy. Maaari mong gamitin ang express template para sa henerasyon kung plano mong i-host ang Cube.js backend sa Heroku o Docker. Inilalarawan ng dokumentasyon ang iba mga paraan ng pagho-host.

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

Ginagamit ang mga variable ng kapaligiran upang i-configure ang access sa database sa cube.js. Ang generator ay gagawa ng .env file kung saan maaari mong tukuyin ang iyong mga susi Atenas.

Ngayon kailangan namin schema ng data, kung saan eksaktong ipahiwatig namin kung paano iniimbak ang aming mga log. Doon ay maaari mo ring tukuyin kung paano kalkulahin ang mga sukatan para sa mga dashboard.

Sa direktoryo schema, gumawa ng file Logs.js. Narito ang isang halimbawa ng modelo ng data para sa nginx:

Code ng modelo

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`
    }
  }
});

Dito ginagamit namin ang variable FILTER_PARAMSupang makabuo ng isang SQL query na may isang partition filter.

Itinakda rin namin ang mga sukatan at parameter na gusto naming ipakita sa dashboard at tumukoy ng mga pre-aggregation. Gagawa ang Cube.js ng mga karagdagang talahanayan na may paunang pinagsama-samang data at awtomatikong ia-update ang data pagdating nito. Hindi lamang nito pinapabilis ang mga query, ngunit binabawasan din ang gastos sa paggamit ng Athena.

Idagdag natin ang impormasyong ito sa file ng schema ng data:

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`
      )
    }
  }
}

Tinukoy namin sa modelong ito na kinakailangang mag-pre-aggregate ng data para sa lahat ng sukatan na ginamit, at gumamit ng partitioning ayon sa buwan. Pre-aggregation partitioning maaaring makabuluhang mapabilis ang pagkolekta at pag-update ng data.

Ngayon ay maaari na nating i-assemble ang dashboard!

Nagbibigay ang Cube.js backend REST API at isang set ng mga library ng kliyente para sa mga sikat na front-end framework. Gagamitin namin ang React na bersyon ng client para buuin ang dashboard. Nagbibigay lang ang Cube.js ng data, kaya kakailanganin namin ng visualization library - Gusto ko ito recharts, ngunit maaari mong gamitin ang anuman.

Tinatanggap ng server ng Cube.js ang kahilingan sa JSON na format, na tumutukoy sa mga kinakailangang sukatan. Halimbawa, upang mabilang kung gaano karaming mga error ang ibinigay ng Nginx sa araw, kailangan mong ipadala ang sumusunod na kahilingan:

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

I-install natin ang Cube.js client at ang React component library sa pamamagitan ng NPM:

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

Nag-import kami ng mga bahagi cubejs ΠΈ QueryRendererupang i-download ang data, at kolektahin ang dashboard:

Dashboard code

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>
        );
      }}
    />
  )
}

Available ang mga mapagkukunan ng dashboard sa code sandbox.

Pinagmulan: www.habr.com

Magdagdag ng komento