Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Normalmente, para la monitorización y el análisis del funcionamiento de Nginx se utilizan productos comerciales o alternativas open-source listas, como Prometheus + Grafana. Esta es una buena opción para la monitorización o análisis en tiempo real, pero no es muy conveniente para el análisis histórico. En cualquier recurso popular, el volumen de datos de los logs de nginx crece rápidamente, y para analizar grandes volúmenes de datos es lógico utilizar algo más especializado.

En este artículo, explicaré cómo se puede utilizar Athena para analizar los logs, tomando como ejemplo Nginx, y mostraré cómo reunir estos datos en un panel de control analítico, utilizando el marco open-source cube.js. Aquí está la arquitectura completa de la solución:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Resumen:
Enlace al panel de control listo.

Para recopilar información, usamos Fluentd, para el procesamiento — AWS Kinesis Data Firehose y AWS Glue, para almacenamiento — AWS S3. Con esta combinación, se pueden almacenar no solo logs de nginx, sino también otros eventos, así como los logs de otros servicios. Puede sustituir algunas partes por equivalentes de su pila, por ejemplo, se pueden enviar logs a kinesis directamente desde nginx, evitando fluentd, o usar logstash para ello.

Recopilando logs de Nginx

Por defecto, los logs de Nginx se ven así:

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

Se pueden analizar, pero es mucho más sencillo ajustar la configuración de Nginx para que entregue 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 para almacenamiento

Para almacenar logs, utilizaremos S3. Esto permite almacenar y analizar logs en un solo lugar, ya que Athena puede trabajar con datos en S3 directamente. Más adelante en el artículo, explicaré cómo organizar y procesar los logs correctamente, pero primero necesitamos un bucket limpio en S3 donde no se almacene nada más. Es recomendable pensar de antemano en en qué región creará el bucket, porque Athena no está disponible en todas las regiones.

Creando un esquema en la consola de Athena

Crearemos una tabla en Athena para los registros. Es necesaria tanto para la escritura como para la lectura, si planea utilizar Kinesis Firehose. Abra la consola de Athena y cree la tabla:

SQL para crear la tabla

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');

Creando un flujo de Kinesis Firehose

Kinesis Firehose registrará los datos obtenidos de Nginx en S3 en el formato seleccionado, organizándolos en directorios con el formato AAAA/MM/DD/HH. Esto será útil al leer los datos. Claro, se puede escribir directamente en S3 desde fluentd, pero en este caso se tendrá que escribir en JSON, lo cual no es eficiente debido al gran tamaño de los archivos. Además, al utilizar PrestoDB o Athena, JSON es el formato de datos más lento. Así que abrimos la consola de Kinesis Firehose, hacemos clic en "Crear flujo de entrega", seleccionamos "direct PUT" en el campo "entrega":

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

En la siguiente pestaña seleccionamos "Conversión de formato de registro" — "Activado" y elegimos "Apache ORC" como formato para la escritura. Según algunas investigaciones de Owen O’Malley, este es el formato óptimo para PrestoDB y Athena. Como esquema, indicamos la tabla que creamos anteriormente. Tenga en cuenta que la ubicación de S3 en Kinesis puede ser cualquier cosa, solo se utiliza el esquema de la tabla. Pero si especifica otra ubicación de S3, no podrá leer esos registros desde esta tabla.

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Elegimos S3 para el almacenamiento y el bucket que creamos anteriormente. Aws Glue Crawler, del cual hablaré más adelante, no puede trabajar con prefijos en el bucket de S3, por lo que es importante dejarlo vacío.

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Las demás opciones se pueden modificar según su carga, generalmente uso las predeterminadas. Tenga en cuenta que la compresión en S3 no está disponible, pero ORC utiliza su propia compresión por defecto.

Fluentd

Ahora que tenemos configurado el almacenamiento y la recepción de logs, necesitamos configurar el envío. Utilizaremos Fluentd, porque me gusta Ruby, pero puede usar Logstash o enviar logs a Kinesis directamente. El servidor Fluentd se puede iniciar de varias maneras; hablaré sobre Docker, porque es simple y conveniente.

Para comenzar, necesitamos un archivo de configuración fluent.conf. Créelo y añade la fuente:

tipo forward
puerto 24224
bind 0.0.0.0

Ahora puedes iniciar el servidor Fluentd. Si necesitas una configuración más avanzada, en Docker Hub hay una guía detallada, incluyendo cómo construir tu propia imagen.

$ 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:stable

Esta configuración utiliza la ruta /fluentd/log para el almacenamiento en caché de registros antes de enviar. Se puede prescindir de esto, pero al reiniciar se pueden perder todos los datos almacenados con esfuerzo. También se puede usar cualquier puerto, 24224 es el puerto predeterminado de Fluentd.

Ahora que tenemos Fluentd en funcionamiento, podemos enviar logs de Nginx allí. Normalmente ejecutamos Nginx en un contenedor Docker, y en este caso Docker tiene un controlador de logs nativo para Fluentd:

$ docker run 
--log-driver=fluentd 
--log-opt fluentd-address=
--log-opt tag="{{.Name}}" 
-v /some/content:/usr/share/nginx/html:ro 
-d 
nginx

Si ejecutas Nginx de otra manera, puedes usar archivos de log, en Fluentd hay file tail plugin.

Añadiremos a la configuración de Fluent el análisis de logs, configurado arriba:

@type parser
  key_name log
  emit_invalid_record_to_error false
  
    @type json

y el envío de logs a Kinesis, utilizando kinesis firehose plugin:

@type kinesis_firehose
    region region
    delivery_stream_name 
    aws_key_id 
    aws_sec_key

Athena

Si has configurado todo correctamente, después de un tiempo (por defecto Kinesis registra los datos recibidos cada 10 minutos) deberías ver los archivos de logs en S3. En el menú "monitoring" de Kinesis Firehose puedes ver cuántos datos se han escrito en S3, así como los errores. No olvides dar acceso de escritura en el bucket S3 para el rol de Kinesis. Si Kinesis no puede analizar algo, almacenará los errores en el mismo bucket.

Ahora puedes ver los datos en Athena. Vamos a buscar las solicitudes recientes que hemos dado como errores:

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

Escaneado de todos los registros en cada solicitud

Ahora nuestros logs están procesados y almacenados en S3 en ORC, comprimidos y listos para análisis. Kinesis Firehose incluso los ha organizado en directorios por cada hora. Sin embargo, mientras la tabla no esté particionada, Athena cargará los datos de todo el tiempo en cada consulta, a excepción de rarezas. Este es un gran problema por dos razones:

  • El volumen de datos sigue creciendo, ralentizando las consultas;
  • El cargo por Athena se basa en el volumen de datos escaneados, con un mínimo de 10 MB por cada consulta.

Para corregir esto, utilizamos AWS Glue Crawler, que escaneará los datos en S3 y registrará información sobre las particiones en Glue Metastore. Esto nos permitirá usar particiones como filtros en las consultas en Athena, y solo escaneará los directorios especificados en la consulta.

Configurando Amazon Glue Crawler

Amazon Glue Crawler escanea todos los datos en el bucket S3 y crea tablas con particiones. Crea un Glue Crawler desde la consola de AWS Glue y añade el bucket donde almacenas los datos. Puedes usar un crawler para varios buckets, en este caso creará tablas en la base de datos especificada con nombres que coincidan con los nombres de los buckets. Si planeas usar estos datos constantemente, no olvides configurar un horario para que el Crawler se ejecute según tus necesidades. Usamos un Crawler para todas las tablas, que se ejecuta cada hora.

Tablas particionadas

Después de la primera ejecución del crawler, deberían aparecer tablas en la base de datos especificada en la configuración para cada bucket escaneado. Abre la consola de Athena y busca la tabla con los logs de Nginx. Vamos a intentar leer algo:

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

Esta consulta seleccionará todos los registros obtenidos entre las 6 y 7 de la mañana del 8 de abril de 2019. Pero, ¿qué tan eficiente es esto en comparación con simplemente leer de una tabla no particionada? Vamos a descubrirlo y seleccionar los mismos registros filtrándolos por timestamp:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

3.59 segundos y 244.34 megabytes de datos en un conjunto de datos que solo tiene una semana de logs. Probemos el filtro por particiones:

Аналитика логов Nginx с помощью Amazon Athena и Cube.js

Un poco más rápido, pero lo más importante — ¡solo 1.23 megabytes de datos! Esto sería mucho más barato de no ser por los 10 megabytes mínimos por consulta en la tarificación. Pero sigue siendo mucho mejor, y en conjuntos de datos más grandes, la diferencia será mucho más impresionante.

Собираем дэшборд с помощью Cube.js

Para construir el dashboard, utilizamos el marco analítico Cube.js. Tiene bastantes funciones, pero nos interesan dos: la capacidad de usar automáticamente filtros por particiones y la pre-agregación de datos. Utiliza un esquema de datos data schema, escrito en Javascript, para generar SQL y ejecutar la consulta en la base de datos. Solo necesitamos indicar cómo usar el filtro por particiones en el esquema de datos.

Crearemos una nueva aplicación Cube.js. Dado que ya estamos utilizando la pila de AWS, es lógico usar Lambda para el despliegue. Puedes utilizar la plantilla express para la generación, si planeas alojar el backend de Cube.js en Heroku o Docker. La documentación describe otros métodos de alojamiento.

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

Para configurar el acceso a la base de datos en cube.js se utilizan variables de entorno. El generador creará un archivo .env, en el cual puedes indicar tus claves para Athena.

Ahora necesitaremos el esquema de datos, en el cual especificaremos cómo se almacenan nuestros registros. También se puede indicar cómo calcular las métricas para los dashboards.

En el directorio schema, crea un archivo Logs.js. Aquí tienes un ejemplo de modelo de datos para nginx:

Código del modelo

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

Aquí utilizamos la variable FILTER_PARAMS, para generar la consulta SQL con un filtro por particiones.

También definimos las métricas y parámetros que queremos mostrar en el dashboard, y especificamos las pre-agregaciones. Cube.js creará tablas adicionales con datos pre-agregados y actualizará los datos automáticamente a medida que lleguen. Esto no solo acelera las consultas, sino que también reduce el costo de uso de Athena.

Agreguemos esta información al archivo del esquema de datos:

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

Especificamos en este modelo que es necesario pre-agregar los datos para todas las métricas utilizadas, y utilizar la partición por meses. Particionamiento de pre-agregaciones puede acelerar significativamente la recopilación y actualización de datos.

¡Ahora podemos crear el panel de control!

El backend de Cube.js proporciona REST API y un conjunto de bibliotecas de cliente para los populares frameworks de frontend. Utilizaremos la versión de React del cliente para construir el panel de control. Cube.js solo proporciona los datos, así que necesitaremos una biblioteca para visualizaciones — a mí me gusta recharts, pero puedes usar cualquiera.

El servidor Cube.js acepta solicitudes en formato JSON, en el que se especifican las métricas necesarias. Por ejemplo, para contar cuántos errores devolvió Nginx por días, necesitamos enviar la siguiente solicitud:

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

Instalaremos el cliente de Cube.js y la biblioteca de componentes React a través de NPM:

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

Importamos los componentes cubejs y QueryRenderer, para descargar los datos, y construimos el panel de control:

Código del panel de control

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 'Cargando...';
        }

        return (
          
            
            
            
          
        );
      }}
    />
  )
}

Los archivos fuente del panel de control están disponibles en CodeSandbox.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster