Di solito, per monitorare e analizzare il funzionamento di Nginx si utilizzano prodotti commerciali o alternative open-source pronte all'uso, come Prometheus + Grafana. Questa è una buona opzione per il monitoraggio o l'analisi in tempo reale, ma non è molto comoda per l'analisi storica. Su qualsiasi risorsa popolare, il volume di 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 è possibile utilizzare per analizzare i log, prendendo come esempio Nginx, e mostrerò come raccogliere un dashboard analitico da questi dati utilizzando il framework open-source cube.js. Ecco l'architettura completa della soluzione:

TL:DR;
.
Per raccogliere informazioni utilizziamo , per il processamento — e , per lo stoccaggio — . Con questa combinazione è 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 i log in kinesis direttamente da nginx, saltando fluentd, oppure utilizzare logstash per questo.
Raccogliamo i log di Nginx
Per impostazione predefinita, i log di Nginx appaiono più o meno così:
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 in modo che produca log in formato 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 lo stoccaggio
Per memorizzare i log, utilizzeremo S3. Questo consente di memorizzare e analizzare i log in un unico posto, poiché Athena può lavorare con i dati in S3 direttamente. Più avanti nell'articolo spiegherò come archiviare e processare i log correttamente, ma per cominciare abbiamo bisogno di un bucket vuoto in S3 in cui non ci sarà nient'altro. È consigliabile pensare in anticipo in quale regione si creerà il bucket, poiché Athena non è disponibile in tutte le regioni.
Creiamo uno schema nella console di Athena
Creeremo una tabella in Athena per i log. È necessaria sia per la registrazione che per la lettura, se prevedi di utilizzare Kinesis Firehose. Apri la console di Athena e crea una 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 uno stream di Kinesis Firehose
Kinesis Firehose scriverà i dati ricevuti da Nginx in S3 nel formato scelto, suddividendoli in directory nel formato AAAA/MM/GG/HH. Questo sarà utile per la lettura dei dati. Certo, è possibile scrivere direttamente in S3 da fluentd, ma in questo caso si dovrà scrivere in JSON, il che è poco efficiente a causa delle grandi dimensioni dei file. Inoltre, utilizzando PrestoDB o Athena, JSON è il formato di dati più lento. Quindi apriamo la console di Kinesis Firehose, facciamo clic su "Crea stream di consegna", selezioniamo "direct PUT" nel campo "delivery":

Nella scheda successiva selezioniamo "Conversione del formato dei record" — "Abilitato" e scegliamo "Apache ORC" come formato per la scrittura. Secondo alcune ricerche di , questo è il formato ottimale per PrestoDB e Athena. Come schema indichiamo la tabella che abbiamo creato in precedenza. Tieni presente che la posizione S3 in kinesis può essere qualsiasi, nella tabella viene utilizzato solo lo schema. Ma se indichi una posizione S3 diversa, non sarà possibile leggere queste registrazioni da questa tabella.

Selezioniamo S3 per l'archiviazione e il bucket che abbiamo creato in precedenza. Aws Glue Crawler, di cui parlerò più avanti, non riesce a lavorare con i prefissi nel bucket S3, quindi è importante lasciarlo vuoto.

Le altre opzioni possono essere modificate a seconda del tuo carico, di solito utilizzo quelle predefinite. Tieni presente che la compressione S3 non è disponibile, ma ORC utilizza una propria compressione per impostazione predefinita.
Fluentd
Ora che abbiamo configurato l'archiviazione e la ricezione dei log, dobbiamo configurare l'invio. Useremo , perché mi piace Ruby, ma puoi usare Logstash o inviare i log direttamente a kinesis. Il server Fluentd può essere avviato in diversi modi, parlerò di docker, perché è semplice e comodo.
Per cominciare, abbiamo bisogno di un file di configurazione fluent.conf. Crea questo file e aggiungi il source:
forward
port 24224
bind 0.0.0.0
Ora puoi avviare il server Fluentd. Se hai bisogno di una configurazione più avanzata, c'è una guida dettagliata, incluso 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:stableQuesta configurazione utilizza il percorso /fluentd/log per la memorizzazione nella cache dei log prima di inviarli. Puoi farne a meno, ma in quel caso, al riavvio, potresti perdere tutto ciò che è stato memorizzato. Puoi usare qualsiasi porta, 24224 è la porta predefinita di Fluentd.
Ora che abbiamo avviato Fluentd, possiamo inviare i log di Nginx lì. 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
nginxSe esegui Nginx in un altro modo, puoi utilizzare i file di log, in Fluentd c'è .
Aggiungiamo alla configurazione Fluent la parsificazione dei log, impostata sopra:
@type parser
key_name log
emit_invalid_record_to_error false
@type jsonE l'invio dei log a Kinesis, utilizzando :
@type kinesis_firehose
region region
delivery_stream_name
aws_key_id
aws_sec_keyAthena
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 scritti in S3 e anche eventuali errori. Non dimenticare di concedere l'accesso in scrittura al bucket S3 per il ruolo Kinesis. Se Kinesis non riesce a elaborare qualcosa, accatasterà gli errori nello stesso bucket.
Ora puoi visualizzare i dati in Athena. Troviamo le richieste recenti per le quali abbiamo restituito errori:
SELECT * FROM "db_name"."table_name" WHERE status > 499 ORDER BY created_at DESC limit 10;Scansionare tutte le registrazioni ad ogni richiesta
Ora i nostri log sono stati elaborati e archiviati in S3 in formato ORC, compressi e pronti per l'analisi. Kinesis Firehose li ha persino organizzati in directory per ogni ora. Tuttavia, finché la tabella non è partizionata, Athena caricherà i dati di tutto il tempo ad ogni richiesta, fatta eccezione di pochi casi. Questo è un grosso problema per due motivi:
- Il volume dei dati cresce costantemente, rallentando le richieste;
- Il costo per Athena è basato sul volume di dati scansionati, con un minimo di 10 MB per ogni richiesta.
Per correggere questo, 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 nelle query in Athena, e scannerà solo le directory specificate nella query.
Configuriamo Amazon Glue Crawler
Amazon Glue Crawler scansiona tutti i dati nel bucket S3 e crea tabelle con partizioni. Crea un Glue Crawler dalla console AWS Glue e aggiungi il bucket in cui memorizzi i dati. Puoi utilizzare un crawler per più bucket, in questo caso creerà tabelle nel database specificato con nomi corrispondenti a quelli dei bucket. Se prevedi di utilizzare questi dati in modo permanente, assicurati di impostare un programma di esecuzione del Crawler in base alle tue esigenze. Utilizziamo un solo Crawler per tutte le tabelle, che viene eseguito ogni ora.
Tabelle partizionate
Dopo il primo avvio del crawler nel database specificato nelle impostazioni, dovrebbero apparire tabelle per ogni bucket scansionato. Apri la console di Athena e trova la tabella con i log di 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à tutte le registrazioni ricevute dalle 6 alle 7 del mattino dell'8 aprile 2019. Ma quanto è più efficiente rispetto alla semplice lettura da una tabella non partizionata? Scopriamolo e selezioniamo le stesse registrazioni, filtrandole in base al timestamp:

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

Un po' più veloce, ma la cosa più importante — solo 1.23 megabyte di dati! Sarebbe stato molto più economico se non ci fosse stata una richiesta minima di 10 megabyte nella tariffazione. Ma è comunque molto meglio, e su dataset più grandi la differenza sarà molto più impressionante.
Собираем дэшборд с помощью Cube.js
Per costruire un dashboard, utilizziamo il framework analitico Cube.js. Ha davvero molte funzionalità, ma noi siamo interessati a due: la possibilità di utilizzare automaticamente i filtri per le partizioni e la pre-aggregazione dei dati. Utilizza uno schema dati , scritto in Javascript, per generare SQL ed eseguire la query sul database. Da noi è richiesto solo di specificare come utilizzare il filtro per le partizioni nello schema dei dati.
Creiamo una nuova applicazione Cube.js. Poiché utilizziamo già lo stack AWS, è logico utilizzare Lambda per il deployment. Puoi utilizzare il template express per la generazione, se prevedi di ospitare il backend di Cube.js su Heroku o Docker. La documentazione descrive altri .
$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athenaPer configurare l'accesso al database in cube.js si utilizzano variabili d'ambiente. Il generatore creerà un file .env, in cui puoi specificare le tue chiavi per .
Ora abbiamo bisogno di , in cui specificheremo come vengono archiviati i nostri log. Qui possiamo anche indicare 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')
`
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 , per generare una query SQL con un filtro sulle partizioni.
Definiamo anche le metriche e i parametri che desideriamo visualizzare sul dashboard e indichiamo le pre-aggregazioni. Cube.js creerà tabelle aggiuntive con dati pre-aggregati e aggiornerà automaticamente i dati in base ai risultati. Questo permette non solo di velocizzare le query, ma anche di ridurre il costo di utilizzo di Athena.
Aggiungiamo queste informazioni al file schema 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`
)
}
}
}Indichiamo in questo modello che è necessario pre-aggregare i dati per tutte le metriche utilizzate e utilizzare la partizione su base mensile. può accelerare significativamente la raccolta e l'aggiornamento dei dati.
Ora possiamo creare il dashboard!
Il backend di Cube.js fornisce e un insieme di librerie client per i popolari framework frontend. Useremo la versione client di React per costruire il dashboard. Cube.js fornisce solo i dati, quindi abbiamo bisogno di una libreria per le visualizzazioni: a me piace , ma puoi usare qualsiasi altra libreria.
Il server di Cube.js accetta richieste in , in cui sono specifiche le metriche necessarie. Ad esempio, per calcolare quante errori ha restituito Nginx nel corso dei giorni, è necessario inviare una richiesta del genere:
{
"measures": ["Logs.errorCount"],
"timeDimensions": [
{
"dimension": "Logs.createdAt",
"dateRange": ["2019-01-01", "2019-01-07"],
"granularity": "day"
}
]
}Installeremo il client Cube.js e la libreria React components tramite NPM:
$ npm i --save @cubejs-client/core @cubejs-client/reactImportiamo i componenti cubejs e QueryRenderer, per estrarre 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 'Caricamento...';
}
return (
);
}}
/>
)
}I sorgenti del dashboard sono disponibili su .
Fonte: habr.com
