En règle générale, des produits commerciaux ou des alternatives open source prêtes à l'emploi, telles que Prometheus + Grafana, sont utilisés pour surveiller et analyser le fonctionnement de Nginx. Il s’agit d’une bonne option pour la surveillance ou l’analyse en temps réel, mais pas très pratique pour l’analyse historique. Sur n'importe quelle ressource populaire, le volume de données des journaux nginx augmente rapidement, et pour analyser une grande quantité de données, il est logique d'utiliser quelque chose de plus spécialisé.
Dans cet article, je vais vous expliquer comment utiliser
TL: DR;
Pour collecter les informations que nous utilisons
Collecte des journaux Nginx
Par défaut, les journaux Nginx ressemblent à ceci :
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" "-"
Ils peuvent être analysés, mais il est beaucoup plus simple de corriger la configuration de Nginx pour qu'elle produise des logs en 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 pour le stockage
Pour stocker les journaux, nous utiliserons S3. Cela vous permet de stocker et d'analyser les journaux en un seul endroit, puisqu'Athena peut travailler directement avec les données dans S3. Plus loin dans l'article, je vous expliquerai comment ajouter et traiter correctement les journaux, mais nous avons d'abord besoin d'un compartiment propre dans S3, dans lequel rien d'autre ne sera stocké. Cela vaut la peine de réfléchir à l'avance dans quelle région vous allez créer votre compartiment, car Athena n'est pas disponible dans toutes les régions.
Créer un circuit dans la console Athena
Créons une table dans Athena pour les journaux. Il est nécessaire à la fois pour l'écriture et la lecture si vous envisagez d'utiliser Kinesis Firehose. Ouvrez la console Athena et créez une table :
Création de tables 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');
Création d'un flux Kinesis Firehose
Kinesis Firehose écrira les données reçues de Nginx vers S3 dans le format sélectionné, en les divisant en répertoires au format AAAA/MM/JJ/HH. Cela sera utile lors de la lecture des données. Vous pouvez bien sûr écrire directement sur S3 depuis fluentd, mais dans ce cas vous devrez écrire du JSON, ce qui est inefficace en raison de la grande taille des fichiers. De plus, lorsque vous utilisez PrestoDB ou Athena, JSON est le format de données le plus lent. Ouvrez donc la console Kinesis Firehose, cliquez sur « Créer un flux de diffusion », sélectionnez « PUT direct » dans le champ « diffusion » :
Dans l'onglet suivant, sélectionnez « Conversion du format d'enregistrement » - « Activé » et sélectionnez « Apache ORC » comme format d'enregistrement. D'après certaines recherches
Nous sélectionnons S3 pour le stockage et le bucket que nous avons créé précédemment. AWS Glue Crawler, dont je parlerai un peu plus tard, ne peut pas fonctionner avec des préfixes dans un bucket S3, il est donc important de le laisser vide.
Les options restantes peuvent être modifiées en fonction de votre charge ; j’utilise généralement celles par défaut. Notez que la compression S3 n'est pas disponible, mais ORC utilise la compression native par défaut.
Courant
Maintenant que nous avons configuré le stockage et la réception des journaux, nous devons configurer l'envoi. Nous utiliserons
Tout d’abord, nous avons besoin du fichier de configuration fluent.conf. Créez-le et ajoutez la source :
Port 24224
lier 0.0.0.0
Vous pouvez maintenant démarrer le serveur Fluentd. Si vous avez besoin d'une configuration plus avancée, accédez à
$ 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
Cette configuration utilise le chemin /fluentd/log
pour mettre en cache les journaux avant l'envoi. Vous pouvez vous en passer, mais lorsque vous redémarrez, vous pouvez perdre tout ce qui est mis en cache avec un travail éreintant. Vous pouvez également utiliser n'importe quel port ; 24224 est le port Fluentd par défaut.
Maintenant que Fluentd est en cours d'exécution, nous pouvons y envoyer des journaux Nginx. Nous exécutons généralement Nginx dans un conteneur Docker, auquel cas Docker dispose d'un pilote de journalisation natif pour 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
Si vous exécutez Nginx différemment, vous pouvez utiliser des fichiers journaux, Fluentd a
Ajoutons l'analyse des journaux configurée ci-dessus à la configuration Fluent :
<filter YOUR-NGINX-TAG.*>
@type parser
key_name log
emit_invalid_record_to_error false
<parse>
@type json
</parse>
</filter>
Et envoyer des journaux à Kinesis en utilisant
<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
Si vous avez tout configuré correctement, après un certain temps (par défaut, Kinesis enregistre les données reçues une fois toutes les 10 minutes), vous devriez voir les fichiers journaux dans S3. Dans le menu « surveillance » de Kinesis Firehose, vous pouvez voir la quantité de données enregistrées dans S3, ainsi que les erreurs. N'oubliez pas de donner un accès en écriture au bucket S3 au rôle Kinesis. Si Kinesis ne parvient pas à analyser quelque chose, il ajoutera les erreurs au même compartiment.
Vous pouvez désormais afficher les données dans Athena. Retrouvons les dernières requêtes pour lesquelles nous avons renvoyé des erreurs :
SELECT * FROM "db_name"."table_name" WHERE status > 499 ORDER BY created_at DESC limit 10;
Analyser tous les enregistrements pour chaque demande
Nos journaux ont désormais été traités et stockés dans S3 dans ORC, compressés et prêts à être analysés. Kinesis Firehose les a même organisés en répertoires pour chaque heure. Cependant, tant que la table n'est pas partitionnée, Athena chargera les données de tous les temps à chaque requête, à de rares exceptions près. C'est un gros problème pour deux raisons :
- Le volume de données ne cesse de croître, ralentissant les requêtes ;
- Athena est facturé en fonction du volume de données analysées, avec un minimum de 10 Mo par requête.
Pour résoudre ce problème, nous utilisons AWS Glue Crawler, qui analysera les données dans S3 et écrira les informations de partition dans Glue Metastore. Cela nous permettra d'utiliser les partitions comme filtre lors de l'interrogation d'Athena, et cela analysera uniquement les répertoires spécifiés dans la requête.
Configuration d'Amazon Glue Crawler
Amazon Glue Crawler analyse toutes les données du compartiment S3 et crée des tables avec des partitions. Créez un Glue Crawler à partir de la console AWS Glue et ajoutez un compartiment dans lequel vous stockez les données. Vous pouvez utiliser un robot pour plusieurs compartiments, auquel cas il créera des tables dans la base de données spécifiée avec des noms qui correspondent aux noms des compartiments. Si vous prévoyez d'utiliser ces données régulièrement, assurez-vous de configurer le calendrier de lancement de Crawler en fonction de vos besoins. Nous utilisons un robot d'exploration pour toutes les tables, qui s'exécute toutes les heures.
Tables partitionnées
Après le premier lancement du robot d'exploration, les tables de chaque bucket analysé doivent apparaître dans la base de données spécifiée dans les paramètres. Ouvrez la console Athena et recherchez le tableau avec les journaux Nginx. Essayons de lire quelque chose :
SELECT * FROM "default"."part_demo_kinesis_bucket"
WHERE(
partition_0 = '2019' AND
partition_1 = '04' AND
partition_2 = '08' AND
partition_3 = '06'
);
Cette requête sélectionnera tous les enregistrements reçus entre 6 h et 7 h le 8 avril 2019. Mais à quel point est-ce plus efficace que la simple lecture à partir d’une table non partitionnée ? Découvrons et sélectionnons les mêmes enregistrements, en les filtrant par horodatage :
3.59 secondes et 244.34 Mo de données sur un ensemble de données avec seulement une semaine de journaux. Essayons un filtre par partition :
Un peu plus rapide, mais surtout - seulement 1.23 mégaoctets de données ! Ce serait beaucoup moins cher sans le minimum de 10 mégaoctets par requête dans la tarification. Mais c’est quand même bien mieux, et sur de grands ensembles de données, la différence sera bien plus impressionnante.
Créer un tableau de bord à l'aide de Cube.js
Pour assembler le tableau de bord, nous utilisons le cadre analytique Cube.js. Il a pas mal de fonctions, mais deux nous intéressent : la possibilité d'utiliser automatiquement des filtres de partition et la pré-agrégation des données. Il utilise un schéma de données
Créons une nouvelle application Cube.js. Puisque nous utilisons déjà la pile AWS, il est logique d'utiliser Lambda pour le déploiement. Vous pouvez utiliser le modèle express pour la génération si vous envisagez d'héberger le backend Cube.js dans Heroku ou Docker. La documentation en décrit d'autres
$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athena
Les variables d'environnement sont utilisées pour configurer l'accès à la base de données dans cube.js. Le générateur créera un fichier .env dans lequel vous pourrez spécifier vos clés pour
Maintenant, nous avons besoin
Dans le répertoire schema
, créez un fichier Logs.js
. Voici un exemple de modèle de données pour nginx :
Code modèle
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`
}
}
});
Ici, nous utilisons la variable
Nous définissons également les métriques et les paramètres que nous souhaitons afficher sur le tableau de bord et spécifions les pré-agrégations. Cube.js créera des tables supplémentaires avec des données pré-agrégées et mettra automatiquement à jour les données dès leur arrivée. Cela accélère non seulement les requêtes, mais réduit également le coût d'utilisation d'Athena.
Ajoutons ces informations au fichier de schéma de données :
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`
)
}
}
}
Nous précisons dans ce modèle qu'il est nécessaire de pré-agréger les données pour toutes les métriques utilisées, et d'utiliser un partitionnement par mois.
Nous pouvons maintenant assembler le tableau de bord !
Le backend Cube.js fournit
Le serveur Cube.js accepte la demande dans
{
"measures": ["Logs.errorCount"],
"timeDimensions": [
{
"dimension": "Logs.createdAt",
"dateRange": ["2019-01-01", "2019-01-07"],
"granularity": "day"
}
]
}
Installons le client Cube.js et la bibliothèque de composants React via NPM :
$ npm i --save @cubejs-client/core @cubejs-client/react
Nous importons des composants cubejs
и QueryRenderer
pour télécharger les données, et collecter le tableau de bord :
Code du tableau de bord
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>
);
}}
/>
)
}
Les sources du tableau de bord sont disponibles sur
Source: habr.com