Analisi dei log Nginx utilizzando Amazon Athena e Cube.js

In genere, per monitorare e analizzare il funzionamento di Nginx vengono utilizzati prodotti commerciali o alternative open source già pronte, come Prometheus + Grafana. Questa è una buona opzione per il monitoraggio o l'analisi in tempo reale, ma non molto conveniente per l'analisi storica. Su qualsiasi risorsa popolare, il volume dei dati provenienti dai log nginx sta crescendo rapidamente e per analizzare una grande quantità di dati è logico utilizzare qualcosa di più specializzato.

In questo articolo ti dirò come puoi usarlo Athena per analizzare i log, prendendo Nginx come esempio, e mostrerò come assemblare una dashboard analitica da questi dati utilizzando il framework open source cube.js. Ecco l'architettura completa della soluzione:

Analisi dei log Nginx utilizzando Amazon Athena e Cube.js

TL: DR;
Collegamento al dashboard finito.

Per raccogliere informazioni che utilizziamo fluente, per l'elaborazione - AWS Kinesis Data Firehose и Colla AWS, per magazzino - AWS S3. Utilizzando questo pacchetto, puoi archiviare non solo i registri nginx, ma anche altri eventi, nonché i registri di altri servizi. Puoi sostituire alcune parti con altre simili per il tuo stack, ad esempio puoi scrivere i log su Kinesis direttamente da nginx, bypassando fluentd, o utilizzare logstash per questo.

Raccolta dei log Nginx

Per impostazione predefinita, i log Nginx hanno un aspetto simile a questo:

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

Possono essere analizzati, ma è molto più semplice correggere la configurazione di Nginx in modo che produca 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 archiviare i log, utilizzeremo S3. Ciò ti consente di archiviare e analizzare i log in un unico posto, poiché Athena può lavorare direttamente con i dati in S3. Più avanti nell'articolo ti dirò come aggiungere ed elaborare correttamente i log, ma prima abbiamo bisogno di un bucket pulito in S3, in cui non verrà archiviato nient'altro. Vale la pena considerare in anticipo in quale regione creerai il tuo bucket, perché Athena non è disponibile in tutte le regioni.

Creazione di un circuito nella console Athena

Creiamo una tabella in Athena per i log. È necessario sia per la scrittura che per la lettura se prevedi di utilizzare Kinesis Firehose. Apri la console Athena e crea una tabella:

Creazione di tabelle 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');

Creazione di Kinesis Firehose Stream

Kinesis Firehose scriverà i dati ricevuti da Nginx a S3 nel formato selezionato, dividendoli in directory nel formato AAAA/MM/GG/HH. Ciò tornerà utile durante la lettura dei dati. Ovviamente puoi scrivere direttamente su S3 da fluentd, ma in questo caso dovrai scrivere JSON, e questo è inefficiente a causa delle grandi dimensioni dei file. Inoltre, quando si utilizza PrestoDB o Athena, JSON è il formato dati più lento. Quindi apri la console Kinesis Firehose, fai clic su “Crea flusso di consegna”, seleziona “PUT diretto” nel campo “consegna”:

Analisi dei log Nginx utilizzando Amazon Athena e Cube.js

Nella scheda successiva, seleziona "Conversione del formato di registrazione" - "Abilitato" e seleziona "Apache ORC" come formato di registrazione. Secondo alcune ricerche Owen O'Malley, questo è il formato ottimale per PrestoDB e Athena. Usiamo la tabella che abbiamo creato sopra come schema. Tieni presente che puoi specificare qualsiasi posizione S3 in Kinesis; dalla tabella viene utilizzato solo lo schema. Ma se specifichi una posizione S3 diversa, non sarai in grado di leggere questi record da questa tabella.

Analisi dei log Nginx utilizzando Amazon Athena e Cube.js

Selezioniamo S3 per lo storage e il bucket che abbiamo creato in precedenza. Aws Glue Crawler, di cui parlerò poco dopo, non può funzionare con i prefissi in un bucket S3, quindi è importante lasciarlo vuoto.

Analisi dei log Nginx utilizzando Amazon Athena e Cube.js

Le restanti opzioni possono essere modificate a seconda del carico; di solito utilizzo quelle predefinite. Tieni presente che la compressione S3 non è disponibile, ma ORC utilizza la compressione nativa per impostazione predefinita.

fluente

Ora che abbiamo configurato l'archiviazione e la ricezione dei log, dobbiamo configurare l'invio. Noi useremo fluente, perché adoro Ruby, ma puoi usare Logstash o inviare i log direttamente a Kinesis. Il server Fluentd può essere lanciato in diversi modi, ti parlerò della docker perché è semplice e comoda.

Innanzitutto, abbiamo bisogno del file di configurazione fluente.conf. Crealo e aggiungi la fonte:

Digitare inoltrare
porto 24224
vincolare 0.0.0.0

Ora puoi avviare il server Fluentd. Se hai bisogno di una configurazione più avanzata, vai a Hub Docker C'è una guida dettagliata, incluso come assemblare la tua immagine.

$ 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

Questa configurazione utilizza il percorso /fluentd/log per memorizzare nella cache i log prima dell'invio. Puoi farne a meno, ma quando riavvii puoi perdere tutto ciò che è stato memorizzato nella cache con un lavoro massacrante. Puoi anche utilizzare qualsiasi porta; 24224 è la porta Fluentd predefinita.

Ora che abbiamo Fluentd in esecuzione, possiamo inviare lì i log Nginx. Di solito eseguiamo Nginx in un contenitore Docker, nel qual caso Docker ha un driver di registrazione nativo per 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

Se esegui Nginx in modo diverso, puoi utilizzare i file di registro, come ha Fluentd plug-in coda file.

Aggiungiamo l'analisi del log configurata sopra alla configurazione Fluent:

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

E invio di log a Kinesis utilizzando plugin Kinesis Firehose:

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

Athena

Se hai configurato tutto correttamente, dopo un po' (per impostazione predefinita, Kinesis Records riceve i dati una volta ogni 10 minuti) dovresti vedere i file di log in S3. Nel menu "monitoraggio" di Kinesis Firehose puoi vedere quanti dati vengono registrati in S3, nonché gli errori. Non dimenticare di concedere l'accesso in scrittura al bucket S3 al ruolo Kinesis. Se Kinesis non riesce ad analizzare qualcosa, aggiungerà gli errori allo stesso bucket.

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

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

Scansione di tutti i record per ciascuna richiesta

Ora i nostri log sono stati elaborati e archiviati in S3 in ORC, compressi e pronti per l'analisi. Kinesis Firehose li ha addirittura organizzati in directory per ogni ora. Tuttavia, finché la tabella non è partizionata, Athena caricherà i dati di tutti i tempi su ogni richiesta, con rare eccezioni. Questo è un grosso problema per due motivi:

  • Il volume dei dati è in costante crescita, rallentando le query;
  • Athena viene fatturato in base al volume di dati scansionati, con un minimo di 10 MB per richiesta.

Per risolvere questo problema, utilizziamo AWS Glue Crawler, che eseguirà la scansione dei dati in S3 e scriverà le informazioni sulla partizione nel Glue Metastore. Ciò ci consentirà di utilizzare le partizioni come filtro durante l'interrogazione di Athena e scansionerà solo le directory specificate nella query.

Configurazione del crawler di Amazon Glue

Amazon Glue Crawler analizza tutti i dati nel bucket S3 e crea tabelle con partizioni. Crea un Glue Crawler dalla console AWS Glue e aggiungi un bucket in cui archivi i dati. Puoi utilizzare un crawler per diversi bucket, nel qual caso creerà tabelle nel database specificato con nomi che corrispondono ai nomi dei bucket. Se prevedi di utilizzare questi dati regolarmente, assicurati di configurare il programma di lancio di Crawler in base alle tue esigenze. Utilizziamo un crawler per tutte le tabelle, che viene eseguito ogni ora.

Tabelle partizionate

Dopo il primo avvio del crawler, le tabelle per ciascun bucket scansionato dovrebbero apparire nel database specificato nelle impostazioni. Apri la console Athena e trova la tabella con i log Nginx. Proviamo a leggere qualcosa:

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

Questa query selezionerà tutti i record ricevuti tra le 6:7 e le 8:2019 dell'XNUMX aprile XNUMX. Ma quanto è più efficiente rispetto alla semplice lettura da una tabella non partizionata? Scopriamo e selezioniamo gli stessi record, filtrandoli per timestamp:

Analisi dei log Nginx utilizzando Amazon Athena e Cube.js

3.59 secondi e 244.34 megabyte di dati su un set di dati con solo una settimana di registri. Proviamo un filtro per partizione:

Analisi dei log Nginx utilizzando Amazon Athena e Cube.js

Un po' più veloce, ma soprattutto: solo 1.23 megabyte di dati! Sarebbe molto più economico se non fosse per il minimo di 10 megabyte per richiesta nel prezzo. Ma è comunque molto meglio, e su set di dati di grandi dimensioni la differenza sarà molto più impressionante.

Creazione di una dashboard utilizzando Cube.js

Per assemblare la dashboard, utilizziamo il framework analitico Cube.js. Ha molte funzioni, ma a noi interessano due: la possibilità di utilizzare automaticamente filtri di partizione e preaggregazione dei dati. Utilizza lo schema dei dati schema dei dati, scritto in Javascript per generare SQL ed eseguire una query sul database. Dobbiamo solo indicare come utilizzare il filtro di partizione nello schema dei dati.

Creiamo una nuova applicazione Cube.js. Poiché stiamo già utilizzando lo stack AWS, è logico utilizzare Lambda per la distribuzione. Puoi utilizzare il modello rapido per la generazione se prevedi di ospitare il backend Cube.js in Heroku o Docker. La documentazione ne descrive altri metodi di hosting.

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

Le variabili di ambiente vengono utilizzate per configurare l'accesso al database in cube.js. Il generatore creerà un file .env in cui puoi specificare le tue chiavi Athena.

Ora abbiamo bisogno schema dei dati, in cui indicheremo esattamente come vengono archiviati i nostri log. Qui puoi anche specificare come calcolare le metriche per i dashboard.

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

Codice modello

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

Qui stiamo usando la variabile FILTER_PARAMper generare una query SQL con un filtro di partizione.

Impostiamo anche le metriche e i parametri che vogliamo visualizzare sulla dashboard e specifichiamo le pre-aggregazioni. Cube.js creerà tabelle aggiuntive con dati preaggregati e aggiornerà automaticamente i dati non appena arrivano. Ciò non solo velocizza 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`
      )
    }
  }
}

In questo modello specifichiamo che è necessario preaggregare i dati per tutte le metriche utilizzate e utilizzare il partizionamento per mese. Partizionamento pre-aggregazione può accelerare notevolmente la raccolta e l'aggiornamento dei dati.

Ora possiamo assemblare il cruscotto!

Il backend Cube.js fornisce API REST e una serie di librerie client per i framework front-end più diffusi. Utilizzeremo la versione React del client per creare la dashboard. Cube.js fornisce solo dati, quindi avremo bisogno di una libreria di visualizzazione: mi piace rigrafici, ma puoi usarne uno qualsiasi.

Il server Cube.js accetta la richiesta in Formato JSON, che specifica le metriche richieste. Ad esempio, per calcolare quanti errori ha dato Nginx ogni giorno, è necessario inviare la seguente richiesta:

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

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

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

Importiamo componenti cubejs и QueryRendererper scaricare i dati e raccogliere la dashboard:

Codice del cruscotto

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

Le fonti del dashboard sono disponibili all'indirizzo CodiceSandbox.

Fonte: habr.com

Aggiungi un commento