Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Di solito, per monitorare e analizzare il funzionamento di Nginx si utilizzano prodotti commerciali o alternative open-source pronte, come Prometheus + Grafana. Questa è una buona opzione per la sorveglianza o l'analisi in tempo reale, ma non è molto comoda per l'analisi storica. Su qualsiasi risorsa popolare, il volume dei dati dai log di Nginx cresce rapidamente, e per analizzare grandi volumi di dati è logico utilizzare qualcosa di più specializzato.

In questo articolo parlerò di come utilizzare Athena per l'analisi dei log, prendendo come esempio Nginx, e mostrerò come raccogliere un dashboard analitico a partire da questi dati, utilizzando il framework open-source cube.js. Ecco l'architettura completa della soluzione:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

TL:DR;
Link al dashboard pronto.

Per raccogliere informazioni utilizziamo Fluentd, per l'elaborazione — AWS Kinesis Data Firehose e AWS Glue, per lo stoccaggio — AWS S3. Con questo insieme è possibile memorizzare non solo i log di Nginx, ma anche altri eventi, così come i log di altri servizi. È possibile sostituire alcune parti con analoghe per il proprio stack, ad esempio, è possibile scrivere direttamente i log in Kinesis da Nginx, bypassando fluentd, o utilizzare Logstash per questo.

Raccogliamo i log di Nginx

Per impostazione predefinita, i log di Nginx appaiono in questo modo:

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

È possibile analizzarli, ma è molto più semplice modificare la configurazione di Nginx per far sì che emetta i log in 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 per l'archiviazione

Per memorizzare i log, utilizzeremo S3. Questo consente di archiviare e analizzare i log in un unico posto, poiché Athena può lavorare direttamente con i dati in S3. Nel seguito dell'articolo spiegherò come organizzare e processare i log, ma per prima cosa abbiamo bisogno di un bucket vuoto in S3, in cui non sarà memorizzato nient'altro. È utile pensare in anticipo a quale regione creerai il bucket, poiché Athena non è disponibile in tutte le regioni.

Creiamo uno schema nella console Athena

Creiamo una tabella in Athena per i log. È necessaria sia per la registrazione che per la lettura, se intendi utilizzare Kinesis Firehose. Apri la console Athena e crea la tabella:

SQL per la creazione della tabella

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');

Creiamo un Kinesis Firehose Stream

Kinesis Firehose registrerà i dati ricevuti da Nginx in S3 nel formato scelto, organizzandoli in directory nel formato YYYY/MM/DD/HH. Questo sarà utile per la lettura dei dati. Certo, si può anche scrivere direttamente in S3 da fluentd, ma in tal caso dovrà essere scritto in JSON, il che non è efficiente a causa delle grandi dimensioni dei file. Inoltre, quando si utilizza PrestoDB o Athena, il JSON è il formato di dati più lento. Quindi apriamo la console Kinesis Firehose, clicchiamo su "Create delivery stream", e selezioniamo "direct PUT" nel campo "delivery":

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Nella scheda successiva selezioniamo "Record format conversion" — "Enabled" e scegliamo "Apache ORC" come formato di registrazione. Secondo alcune ricerche Owen O’Malley, questo è il formato ottimale per PrestoDB e Athena. Come schema, indichiamo la tabella che abbiamo creato in precedenza. Si noti che la posizione S3 in Kinesis può essere qualsiasi, poiché dalla tabella viene utilizzato solo lo schema. Ma se si indica una posizione S3 diversa, non sarà possibile leggere quelle registrazioni da questa tabella.

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Selezioniamo S3 per lo storage e il bucket che abbiamo creato in precedenza. Aws Glue Crawler, di cui parlerò tra poco, non sa gestire i prefissi nel bucket S3, quindi è importante lasciarlo vuoto.

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Le altre opzioni possono essere modificate a seconda del vostro carico, io di solito utilizzo quelle predefinite. Nota che la compressione S3 non è disponibile, ma ORC utilizza la propria compressione di default.

Fluentd

Adesso che abbiamo impostato la memorizzazione e il recupero dei log, dobbiamo configurare l'invio. Useremo Fluentd, perché mi piace Ruby, ma puoi usare Logstash o inviare direttamente i log a Kinesis. Il server Fluentd può essere avviato in vari modi, parlerò di Docker perché è semplice e conveniente.

Per iniziare, abbiamo bisogno di un file di configurazione fluent.conf. Crealo e aggiungi il source:

type forward
port 24224
bind 0.0.0.0

Adesso possiamo avviare il server Fluentd. Se hai bisogno di una configurazione più avanzata, su Docker Hub c'è una guida dettagliata, compresa quella su come costruire la tua immagine.

$ 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:stable

Questa configurazione utilizza il percorso /fluentd/log per la memorizzazione dei log prima dell'invio. Puoi farne a meno, ma in quel caso potresti perdere tutto ciò che è stato memorizzato in cache dopo un riavvio. Puoi usare qualsiasi porta, 24224 è la porta predefinita di Fluentd.

Ora che abbiamo avviato Fluentd, possiamo inviare i log di Nginx. Di solito eseguiamo Nginx in un contenitore Docker, e in questo caso Docker ha un driver di log nativo per Fluentd:

$ docker run 
--log-driver=fluentd 
--log-opt fluentd-address=
--log-opt tag="{{.Name}}" 
-v /some/content:/usr/share/nginx/html:ro 
-d 
nginx

Se stai eseguendo Nginx in un altro modo, puoi utilizzare i log, in Fluentd c'è il plugin di file tail.

Aggiungiamo alla configurazione Fluent il parsing dei log configurato in precedenza:

@type parser
  key_name log
  emit_invalid_record_to_error false
  
    @type json

E inviare i log a Kinesis, utilizzando il plugin kinesis firehose:

@type kinesis_firehose
    region region
    delivery_stream_name 
    aws_key_id 
    aws_sec_key

Athena

Se hai configurato tutto correttamente, dopo un po' (per impostazione predefinita Kinesis registra i dati ricevuti ogni 10 minuti) dovresti vedere i file di log in S3. Nel menu "monitoring" di Kinesis Firehose puoi vedere quanti dati sono stati registrati in S3 e anche eventuali errori. Non dimenticare di concedere accesso in scrittura al bucket S3 per il ruolo Kinesis. Se Kinesis non riesce a decifrare qualcosa, registrerà gli errori nello stesso bucket.

Ora puoi visualizzare i dati in Athena. Troviamo le query recenti per le quali abbiamo restituito errori:

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

Scansione di tutte le registrazioni per ogni query

Ora i nostri log sono stati elaborati e archiviati in S3 in ORC, compressi e pronti per l'analisi. Kinesis Firehose li ha anche suddivisi in directory per ogni ora. Tuttavia, finché la tabella non è partizionata, Athena caricherà i dati di tutto il tempo per ogni query, salvo rare eccezioni. Questo è un problema significativo per due motivi:

  • Il volume dei dati cresce costantemente, rallentando le query;
  • Il costo di Athena è calcolato in base al volume di dati scansionati, con un minimo di 10 MB per ogni query.

Per risolvere questo problema, utilizziamo AWS Glue Crawler, che scansionerà i dati in S3 e registrerà le informazioni sulle partizioni nel Glue Metastore. Questo ci permetterà di utilizzare le partizioni come filtro durante le query in Athena, scansionando solo le directory specificate nella query.

Configuriamo Amazon Glue Crawler

Amazon Glue Crawler scans all data in the S3 bucket and creates tables with partitions. Create a Glue Crawler from the AWS Glue console and add the bucket where you store your data. You can use one crawler for multiple buckets; in this case, it will create tables in the specified database with names matching the bucket names. If you plan to use this data regularly, be sure to set up a schedule for the crawler according to your needs. We use one crawler for all tables, which runs every hour.

Tabelle partizionate

After the crawler's first run in the database specified in the settings, tables for each scanned bucket should appear. Open the Athena console and look for the table with Nginx logs. Let's try to read something:

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

This query will select all records received from 6 to 7 AM on April 8, 2019. But how much more efficient is this compared to simply reading from a non-partitioned table? Let's find out and select the same records, filtering them by timestamp:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

3.59 secondi e 244.34 megabyte di dati su un dataset con solo una settimana di log. Proviamo un filtro per partizioni:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Un po' più veloce, ma la cosa più importante è che si tratta di soli 1.23 megabyte di dati! Sarebbe stato molto più economico se non ci fosse il minimo di 10 megabyte per richiesta nel pricing. Ma è comunque molto meglio, e su dataset più grandi la differenza sarà decisamente più impressionante.

Собираем дэшборд с помощью Cube.js

Per costruire il dashboard, utilizziamo il framework analitico Cube.js. Ha abbastanza funzioni, ma a noi interessano due: la possibilità di utilizzare automaticamente i filtri per partizioni e la pre-aggregazione dei dati. Utilizza uno schema dati data schema, scritto in Javascript, per generare SQL ed eseguire la query sul database. Dobbiamo solo specificare come utilizzare il filtro per partizioni nello schema dati.

Creiamo una nuova applicazione Cube.js. Poiché stiamo già utilizzando lo stack AWS, è logico usare Lambda per il deploy. Puoi utilizzare il template express per generare, se intendi ospitare il backend di Cube.js su Heroku o Docker. La documentazione descrive altri metodi di hosting.

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

Per configurare l'accesso al database in cube.js, si utilizzano le variabili d'ambiente. Il generatore creerà un file .env dove puoi specificare le tue chiavi per Athena.

Ora avremo bisogno di uno schema dei dati, in cui indicheremo come vengono memorizzati i nostri log. Qui è possibile anche specificare come calcolare le metriche per i dashboard.

Nella directory schema, crea un file Logs.js. Ecco un esempio di modello di dati per nginx:

Codice del modello

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: `Yes`
        }],
        else: { label: `No` }
      }
    },

    createdAt: {
      sql: `from_unixtime(created_at)`,
      type: `time`
    }
  }
});

Qui utilizziamo la variabile FILTER_PARAMS, per generare una query SQL con filtro sulle partizioni.

Impostiamo anche le metriche e i parametri che vogliamo visualizzare nel dashboard e definiamo le pre-aggregazioni. Cube.js creerà tabelle aggiuntive con dati pre-aggregati e aggiornerà automaticamente i dati man mano che arrivano. Questo non solo accelera le query, ma riduce anche i costi di utilizzo di Athena.

Aggiungiamo queste informazioni al file dello schema dei dati:

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

Specifichiamo in questo modello che è necessario pre-aggregare i dati per tutte le metriche utilizzate e utilizzare il partizionamento per mesi. Partizionamento delle pre-aggregazioni può accelerare significativamente la raccolta e l'aggiornamento dei dati.

Ora possiamo costruire il dashboard!

Il backend di Cube.js fornisce REST API e un set di librerie client per i popolari framework frontend. Utilizzeremo la versione React del client per costruire il dashboard. Cube.js fornisce solo i dati, quindi avremo bisogno di una libreria per le visualizzazioni — a me piace recharts, ma puoi usare qualsiasi altra.

Il server Cube.js accetta richieste in formato JSON, in cui sono specifiche le metriche necessarie. Ad esempio, per calcolare quante errori ha restituito Nginx per giorno, è necessario inviare una richiesta del tipo:

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

Installeremo il client Cube.js e la libreria dei componenti React tramite NPM:

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

Importiamo i componenti cubejs e QueryRenderer, per caricare i dati, e costruiamo il dashboard:

Il codice del dashboard

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 'Loading...';
        }

        return (
          
            
            
            
          
        );
      }}
    />
  )
}

I sorgenti del dashboard sono disponibili su CodeSandbox.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster