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

Resumen:
.
Para recopilar información, usamos , para el procesamiento — y , para almacenamiento — . 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":

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

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.

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 , 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:
forward
puerto 24224
bind 0.0.0.0
Ahora puedes iniciar el servidor Fluentd. Si necesitas una configuración más avanzada, en 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:stableEsta 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
nginxSi ejecutas Nginx de otra manera, puedes usar archivos de log, en Fluentd hay .
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 jsony el envío de logs a Kinesis, utilizando :
@type kinesis_firehose
region region
delivery_stream_name
aws_key_id
aws_sec_keyAthena
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:

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:

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 , 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 .
$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athenaPara 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 .
Ahora necesitaremos , 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 , 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. puede acelerar significativamente la recopilación y actualización de datos.
¡Ahora podemos crear el panel de control!
El backend de Cube.js proporciona 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 , pero puedes usar cualquiera.
El servidor Cube.js acepta solicitudes en , 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/reactImportamos 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 .
Fuente: habr.com
