Normalerweise verwenden wir kommerzielle Produkte oder fertige Open-Source-Alternativen wie Prometheus + Grafana, um die Leistung von Nginx zu überwachen und zu analysieren. Dies ist eine gute Option für das Monitoring oder die Echtzeitanalyse, jedoch nicht sehr praktisch für die historische Analyse. Auf jeder beliebten Plattform wächst das Datenvolumen aus Nginx-Logs schnell, und es ist sinnvoll, für die Analyse großer Datenmengen etwas Spezielleres zu verwenden.
In diesem Artikel erkläre ich, wie man zur Analyse von Logs verwenden kann, am Beispiel von Nginx, und ich zeige, wie man aus diesen Daten ein analytisches Dashboard erstellt, unter Verwendung des Open-Source-Frameworks cube.js. Hier ist die vollständige Architektur der Lösung:

TL:DR;
.
Zur Informationssammlung verwenden wir , zur Verarbeitung — und , zum Speichern — . Mit diesem Zusammenspiel können nicht nur die Nginx-Logs, sondern auch andere Ereignisse sowie Logs anderer Dienste gespeichert werden. Sie können bestimmte Teile durch ähnliche für Ihren Stack ersetzen, z.B. können Sie die Logs direkt aus Nginx in Kinesis schreiben, ohne fluentd zu verwenden, oder logstash dafür einsetzen.
Wir sammeln Nginx-Logs
Standardmäßig sehen die Nginx-Logs ungefähr so aus:
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" "-"Sie können sie parsen, aber es ist viel einfacher, die Nginx-Konfiguration so zu ändern, dass sie Logs im JSON-Format ausgibt:
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 zur Speicherung
Um die Logs zu speichern, verwenden wir S3. Dies ermöglicht es, Logs an einem Ort zu speichern und zu analysieren, da Athena direkt mit Daten in S3 arbeiten kann. Im weiteren Verlauf des Artikels werde ich erklären, wie man die Logs richtig ablegt und verarbeitet, aber zunächst benötigen wir einen leeren Bucket in S3, in dem nichts anderes gespeichert wird. Es ist ratsam, im Voraus darüber nachzudenken, in welcher Region Sie den Bucket erstellen, da Athena nicht in allen Regionen verfügbar ist.
Wir erstellen ein Schema in der Athena-Konsole
Wir werden eine Tabelle in Athena für Logs erstellen. Sie wird sowohl zum Schreiben als auch zum Lesen benötigt, wenn Sie planen, Kinesis Firehose zu verwenden. Öffnen Sie die Athena-Konsole und erstellen Sie eine Tabelle:
SQL zur Erstellung der Tabelle
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');Wir erstellen einen Kinesis Firehose Stream
Kinesis Firehose wird die von Nginx empfangenen Daten in S3 im gewählten Format schreiben und dabei nach Verzeichnissen im Format JJJJ/MM/TT/HH aufteilen. Dies wird beim Lesen der Daten hilfreich sein. Natürlich könnte man auch direkt aus fluentd in S3 schreiben, aber in diesem Fall müsste man JSON schreiben, was aufgrund der großen Dateigröße ineffizient ist. Außerdem ist JSON, wenn man PrestoDB oder Athena verwendet, das langsamste Datenformat. Also öffnen wir die Kinesis Firehose-Konsole, klicken auf „Create delivery stream“ und wählen „direct PUT“ im Feld „delivery“:

Im nächsten Tab wählen wir „Record format conversion“ – „Enabled“ und wählen „Apache ORC“ als Format zum Schreiben. Laut einigen Studien , ist dies das optimale Format für PrestoDB und Athena. Als Schema geben wir die oben erstellte Tabelle an. Beachten Sie, dass der S3-Standort in Kinesis beliebig angegeben werden kann, es wird nur das Schema der Tabelle verwendet. Wenn Sie jedoch einen anderen S3-Standort angeben, können diese Datensätze aus dieser Tabelle nicht gelesen werden.

Wir wählen S3 zur Speicherung und den Bucket, den wir zuvor erstellt haben. Der AWS Glue Crawler, über den ich später sprechen werde, kann nicht mit Präfixen im S3-Bucket umgehen, daher ist es wichtig, diesen leer zu lassen.

Die restlichen Optionen können je nach Ihrer Last angepasst werden, ich verwende normalerweise die Standardwerte. Beachten Sie, dass die Kompression in S3 nicht verfügbar ist, aber ORC standardmäßig eine eigene Kompression verwendet.
Fluentd
Jetzt, da wir die Speicherung und den Empfang von Logs eingerichtet haben, müssen wir die Sendung einrichten. Wir werden , weil ich Ruby mag, aber Sie können auch Logstash verwenden oder Logs direkt nach Kinesis senden. Der Fluentd-Server kann auf verschiedene Weise gestartet werden; ich werde über Docker sprechen, weil es einfach und bequem ist.
Zunächst benötigen wir eine Konfigurationsdatei fluent.conf. Erstellen Sie diese und fügen Sie die Quelle hinzu:
forward
port 24224
bind 0.0.0.0
Jetzt kann der Fluentd-Server gestartet werden. Wenn Sie eine fortgeschrittenere Konfiguration benötigen, finden Sie eine detaillierte Anleitung, auch wie man sein eigenes Image erstellt.
$ 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:stableDiese Konfiguration verwendet den Pfad /fluentd/log zum Cachen von Logs vor dem Senden. Es ist nicht zwingend erforderlich, aber sonst könnten beim Neustart alle hart erarbeiteten Caches verloren gehen. Auch der Port kann beliebig sein, 24224 ist der Standardport von Fluentd.
Jetzt, wo wir Fluentd laufen haben, können wir die Nginx-Logs dorthin senden. Wir führen normalerweise Nginx in einem Docker-Container aus, und in diesem Fall hat Docker einen nativen Log-Treiber für Fluentd:
$ docker run
--log-driver=fluentd
--log-opt fluentd-address=
--log-opt tag="{{.Name}}"
-v /some/content:/usr/share/nginx/html:ro
-d
nginxWenn Sie Nginx anders ausführen, können Sie Log-Dateien verwenden; in Fluentd gibt es das .
Fügen wir der Fluent-Konfiguration das oben angegebene Log-Parsing hinzu:
@type parser
key_name log
emit_invalid_record_to_error false
@type jsonUnd senden der Logs an Kinesis, unter Verwendung des :
@type kinesis_firehose
region region
delivery_stream_name
aws_key_id
aws_sec_keyAthena
Wenn Sie alles richtig konfiguriert haben, sollten Sie nach einiger Zeit (standardmäßig schreibt Kinesis alle 10 Minuten die empfangenen Daten) die Log-Dateien in S3 sehen. Im Menü "Monitoring" von Kinesis Firehose können Sie sehen, wie viele Daten in S3 geschrieben wurden, sowie Fehler. Vergessen Sie nicht, der Kinesis-Rolle Schreibzugriff auf den S3-Bucket zu geben. Wenn Kinesis etwas nicht parsen konnte, wird es die Fehler im selben Bucket speichern.
Jetzt können die Daten in Athena angesehen werden. Lassen Sie uns die neuesten Abfragen finden, für die wir Fehler abgegeben haben:
SELECT * FROM "db_name"."table_name" WHERE status > 499 ORDER BY created_at DESC limit 10;Scannen aller Einträge bei jeder Abfrage
Unsere Logs sind jetzt verarbeitet und in S3 im ORC-Format gespeichert, komprimiert und bereit für die Analyse. Kinesis Firehose hat sie sogar nach Verzeichnissen für jede Stunde organisiert. Solange die Tabelle jedoch nicht partitioniert ist, wird Athena bei jeder Abfrage alle Daten aus der gesamten Zeit laden, mit wenigen Ausnahmen. Das ist ein großes Problem aus zwei Gründen:
- Das Datenvolumen wächst ständig und verlangsamt die Abfragen;
- Die Rechnung für Athena wird abhängig vom Umfang der gescannten Daten ausgestellt, mit einem Minimum von 10 MB pro Anfrage.
Um dies zu beheben, verwenden wir den AWS Glue Crawler, der die Daten in S3 scannt und Informationen über Partitionen im Glue Metastore speichert. Dies ermöglicht es uns, Partitionen als Filter bei Anfragen in Athena zu verwenden, sodass nur die im Anfrage angegebenen Verzeichnisse gescannt werden.
Amazon Glue Crawler einrichten
Der Amazon Glue Crawler scannt alle Daten in einem S3-Bucket und erstellt Tabellen mit Partitionen. Erstellen Sie einen Glue Crawler aus der AWS Glue-Konsole und fügen Sie den Bucket hinzu, in dem Sie die Daten speichern. Sie können einen Crawler für mehrere Buckets verwenden; in diesem Fall erstellt er Tabellen in der angegebenen Datenbank mit Namen, die mit den Namen der Buckets übereinstimmen. Wenn Sie diese Daten dauerhaft nutzen möchten, vergessen Sie nicht, einen Zeitplan für den Start des Crawlers entsprechend Ihren Bedürfnissen einzustellen. Wir verwenden einen Crawler für alle Tabellen, der jede Stunde ausgeführt wird.
Partitionierte Tabellen
Nach dem ersten Start des Crawlers sollten in der Datenbank, die in den Einstellungen angegeben ist, Tabellen für jeden gescannten Bucket erscheinen. Öffnen Sie die Athena-Konsole und suchen Sie die Tabelle mit den Nginx-Protokollen. Lassen Sie uns versuchen, etwas zu lesen:
SELECT * FROM "default"."part_demo_kinesis_bucket"
WHERE(
partition_0 = '2019' AND
partition_1 = '04' AND
partition_2 = '08' AND
partition_3 = '06'
);Diese Anfrage wählt alle Datensätze aus, die zwischen 6 und 7 Uhr am 8. April 2019 erhalten wurden. Aber wie viel effizienter ist das im Vergleich zum einfachen Lesen aus einer nicht partitionierten Tabelle? Lassen Sie uns das herausfinden und dieselben Datensätze auswählen, indem wir sie nach Zeitstempel filtern:

3,59 Sekunden und 244,34 Megabyte Daten für einen Datensatz, in dem es nur eine Woche Protokolle gibt. Versuchen wir den Filter nach Partitionen:

Ein wenig schneller, aber das Wichtigste — nur 1,23 Megabyte an Daten! Das wäre viel günstiger, wenn nicht die Mindestanforderung von 10 Megabyte pro Anfrage im Preismodell wäre. Aber es ist trotzdem viel besser, und bei großen Datensätzen wird der Unterschied viel beeindruckender sein.
Собираем дэшборд с помощью Cube.js
Um ein Dashboard zu erstellen, verwenden wir das Analyseframework Cube.js. Es hat viele Funktionen, aber uns interessieren zwei: die automatische Verwendung von Filtern nach Partitionen und die Voraggregation von Daten. Es verwendet ein Datenschema , geschrieben in Javascript, um SQL zu generieren und eine Anfrage an die Datenbank auszuführen. Von uns wird nur verlangt, anzugeben, wie der Filter für Partitionen im Datenschema verwendet wird.
Wir erstellen eine neue Cube.js-Anwendung. Da wir bereits das AWS-Stack verwenden, ist es logisch, Lambda für das Deployment zu nutzen. Sie können die Express-Vorlage zur Generierung verwenden, wenn Sie planen, das Cube.js-Backend in Heroku oder Docker zu hosten. In der Dokumentation sind andere .
$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athenaFür die Konfiguration des Zugangs zur Datenbank in cube.js werden Umgebungsvariablen verwendet. Der Generator erstellt eine .env-Datei, in der Sie Ihre Schlüssel für .
Jetzt benötigen wir , in dem wir angeben, wie unsere Logs gespeichert werden. Dort können auch die Metriken für die Dashboards berechnet werden.
Im Verzeichnis schema, erstellen Sie eine Datei Logs.js. Hier ist ein Beispiel für ein Datenmodell für Nginx:
Modellcode
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`
}
}
});Hier verwenden wir die Variable , um eine SQL-Anfrage mit einem Filter für Partitionen zu generieren.
Wir definieren auch die Metriken und Parameter, die wir auf dem Dashboard anzeigen möchten, und geben die Pre-Aggregationen an. Cube.js erstellt zusätzliche Tabellen mit aggregierten Daten und aktualisiert diese automatisch, während die Daten eingehen. Dies beschleunigt nicht nur die Anfragen, sondern senkt auch die Kosten für die Nutzung von Athena.
Fügen wir diese Informationen in die Datei des Datenschemas ein:
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`
)
}
}
}Wir geben in diesem Modell an, dass alle verwendeten Metriken voraggregiert werden müssen und dass die Partitionierung nach Monaten verwendet werden muss. kann die Sammlung und Aktualisierung von Daten erheblich beschleunigen.
Jetzt können wir das Dashboard erstellen!
Das Backend von Cube.js bietet und eine Reihe von Client-Bibliotheken für beliebte Frontend-Frameworks. Wir werden die React-Version des Clients verwenden, um das Dashboard zu erstellen. Cube.js stellt nur die Daten zur Verfügung, daher benötigen wir eine Bibliothek für Visualisierungen – ich mag , aber Sie können jede beliebige verwenden.
Der Server von Cube.js akzeptiert Anfragen im , in dem die erforderlichen Metriken angegeben sind. Um beispielsweise zu berechnen, wie viele Fehler Nginx pro Tag zurückgegeben hat, muss folgende Anfrage gesendet werden:
{
"measures": ["Logs.errorCount"],
"timeDimensions": [
{
"dimension": "Logs.createdAt",
"dateRange": ["2019-01-01", "2019-01-07"],
"granularity": "day"
}
]
}Wir installieren den Cube.js-Client und die React-Komponentenbibliothek über NPM:
$ npm i --save @cubejs-client/core @cubejs-client/reactWir importieren die Komponenten cubejs und QueryRenderer, um die Daten abzurufen, und erstellen das Dashboard:
Der Code des Dashboards
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 'Lädt...';
}
return (
);
}}
/>
)
}Die Quellcodes des Dashboards sind verfügbar auf .
Quelle: habr.com
