Analyse des journaux Nginx à l'aide d'Amazon Athena et Cube.js

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 Athena pour analyser les journaux, en prenant Nginx comme exemple, et je montrerai comment assembler un tableau de bord analytique à partir de ces données à l'aide du framework open source cube.js. Voici l’architecture complète de la solution :

Analyse des journaux Nginx à l'aide d'Amazon Athena et Cube.js

TL: DR;
Lien vers le tableau de bord terminé.

Pour collecter les informations que nous utilisons Courant, pour traitement - AWS Kinesis Data Firehose и Colle AWS, pour le stockage - AWS S3. En utilisant ce bundle, vous pouvez stocker non seulement les journaux nginx, mais également d'autres événements, ainsi que les journaux d'autres services. Vous pouvez remplacer certaines parties par des pièces similaires pour votre pile, par exemple, vous pouvez écrire des journaux dans Kinesis directement à partir de nginx, en contournant fluentd, ou utiliser logstash pour cela.

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

Analyse des journaux Nginx à l'aide d'Amazon Athena et Cube.js

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 Owen O'Malley, c'est le format optimal pour PrestoDB et Athena. Nous utilisons le tableau que nous avons créé ci-dessus comme schéma. Veuillez noter que vous pouvez spécifier n'importe quel emplacement S3 dans Kinesis ; seul le schéma est utilisé à partir du tableau. Mais si vous spécifiez un emplacement S3 différent, vous ne pourrez pas lire ces enregistrements à partir de cette table.

Analyse des journaux Nginx à l'aide d'Amazon Athena et Cube.js

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.

Analyse des journaux Nginx à l'aide d'Amazon Athena et Cube.js

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 Courant, parce que j'aime Ruby, mais vous pouvez utiliser Logstash ou envoyer des journaux directement à Kinesis. Le serveur Fluentd peut être lancé de plusieurs manières, je vais vous parler de docker car c'est simple et pratique.

Tout d’abord, nous avons besoin du fichier de configuration fluent.conf. Créez-le et ajoutez la source :

type :
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 Hub Il existe un guide détaillé, expliquant comment assembler votre image.

$ 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 plugin de queue de fichier.

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

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 :

Analyse des journaux Nginx à l'aide d'Amazon Athena et Cube.js

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 :

Analyse des journaux Nginx à l'aide d'Amazon Athena et Cube.js

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 schéma de données, écrit en Javascript pour générer du SQL et exécuter une requête de base de données. Il suffit d'indiquer comment utiliser le filtre de partition dans le 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 méthodes d'hébergement.

$ 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 Athena.

Maintenant, nous avons besoin schéma de données, dans lequel nous indiquerons exactement comment nos journaux sont stockés. Vous pouvez également y spécifier comment calculer les métriques pour les tableaux de bord.

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 FILTER_PARAMSpour générer une requête SQL avec un filtre de partition.

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. Partitionnement de pré-agrégation peut considérablement accélérer la collecte et la mise à jour des données.

Nous pouvons maintenant assembler le tableau de bord !

Le backend Cube.js fournit API REST et un ensemble de bibliothèques clientes pour les frameworks frontaux populaires. Nous utiliserons la version React du client pour construire le tableau de bord. Cube.js ne fournit que des données, nous aurons donc besoin d'une bibliothèque de visualisation - j'aime ça retrace, mais vous pouvez en utiliser n'importe lequel.

Le serveur Cube.js accepte la demande dans Format JSON, qui spécifie les métriques requises. Par exemple, pour calculer le nombre d'erreurs générées par Nginx par jour, vous devez envoyer la requête suivante :

{
  "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 и QueryRendererpour 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 bac à sable de code.

Source: habr.com

Ajouter un commentaire