Análise de log Nginx usando Amazon Athena e Cube.js

Normalmente, produtos comerciais ou alternativas de código aberto prontas, como Prometheus + Grafana, são usados ​​para monitorar e analisar a operação do Nginx. Esta é uma boa opção para monitoramento ou análise em tempo real, mas não muito conveniente para análise histórica. Em qualquer recurso popular, o volume de dados dos logs do nginx está crescendo rapidamente e, para analisar uma grande quantidade de dados, é lógico usar algo mais especializado.

Neste artigo vou dizer como você pode usar Atena para analisar logs, tomando o Nginx como exemplo, e mostrarei como montar um dashboard analítico a partir desses dados usando o framework cube.js de código aberto. Aqui está a arquitetura completa da solução:

Análise de log Nginx usando Amazon Athena e Cube.js

TL: DR;
Link para o painel finalizado.

Para coletar informações que usamos Fluente, Para processamento - AWS Kinesis Data Firehose и Cola AWS, para armazenamento - AWS S3. Usando este pacote, você pode armazenar não apenas logs nginx, mas também outros eventos, bem como logs de outros serviços. Você pode substituir algumas partes por outras semelhantes para sua pilha, por exemplo, você pode gravar logs no kinesis diretamente do nginx, ignorando o fluentd, ou usar o logstash para isso.

Coletando registros do Nginx

Por padrão, os logs do Nginx são mais ou menos assim:

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

Eles podem ser analisados, mas é muito mais fácil corrigir a configuração do Nginx para que produza logs em 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 armazenamento

Para armazenar logs, usaremos S3. Isso permite armazenar e analisar logs em um só lugar, já que o Athena pode trabalhar diretamente com dados no S3. Posteriormente neste artigo, direi como adicionar e processar logs corretamente, mas primeiro precisamos de um bucket limpo no S3, no qual nada mais será armazenado. Vale a pena considerar antecipadamente em qual região você criará seu bucket, pois o Athena não está disponível em todas as regiões.

Criando um circuito no console do Athena

Vamos criar uma tabela no Athena para logs. Ele é necessário para escrever e ler se você planeja usar o Kinesis Firehose. Abra o console do Athena e crie uma tabela:

Criação de tabela 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');

Criando fluxo do Kinesis Firehose

O Kinesis Firehose gravará os dados recebidos do Nginx no S3 no formato selecionado, dividindo-os em diretórios no formato AAAA/MM/DD/HH. Isso será útil ao ler dados. Você pode, é claro, escrever diretamente no S3 a partir do fluentd, mas neste caso você terá que escrever JSON, e isso é ineficiente devido ao grande tamanho dos arquivos. Além disso, ao usar PrestoDB ou Athena, JSON é o formato de dados mais lento. Portanto, abra o console do Kinesis Firehose, clique em “Criar fluxo de entrega”, selecione “PUT direto” no campo “entrega”:

Análise de log Nginx usando Amazon Athena e Cube.js

Na próxima aba, selecione “Conversão de formato de gravação” - “Ativado” e selecione “Apache ORC” como formato de gravação. De acordo com algumas pesquisas Owen O'Malley, este é o formato ideal para PrestoDB e Athena. Usamos a tabela que criamos acima como esquema. Observe que você pode especificar qualquer local do S3 no kinesis; somente o esquema da tabela é usado. Mas se você especificar um local S3 diferente, não poderá ler esses registros nesta tabela.

Análise de log Nginx usando Amazon Athena e Cube.js

Selecionamos S3 para armazenamento e o bucket que criamos anteriormente. O Aws Glue Crawler, do qual falarei um pouco mais tarde, não pode trabalhar com prefixos em um bucket S3, por isso é importante deixá-lo vazio.

Análise de log Nginx usando Amazon Athena e Cube.js

As demais opções podem ser alteradas dependendo da sua carga; costumo usar as padrão. Observe que a compactação S3 não está disponível, mas o ORC usa compactação nativa por padrão.

Fluente

Agora que configuramos o armazenamento e o recebimento de logs, precisamos configurar o envio. Nós vamos usar Fluente, porque adoro Ruby, mas você pode usar o Logstash ou enviar logs diretamente para o kinesis. O servidor Fluentd pode ser iniciado de várias maneiras, vou falar sobre o docker porque é simples e conveniente.

Primeiro, precisamos do arquivo de configuração fluent.conf. Crie-o e adicione a fonte:

tipo para a frente
port 24224
vincular 0.0.0.0

Agora você pode iniciar o servidor Fluentd. Se você precisar de uma configuração mais avançada, vá para Hub do Docker Há um guia detalhado, incluindo como montar sua imagem.

$ 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

Esta configuração usa o caminho /fluentd/log para armazenar logs em cache antes de enviar. Você pode passar sem isso, mas ao reiniciar, poderá perder tudo o que está em cache com um trabalho árduo. Você também pode usar qualquer porta; 24224 é a porta padrão do Fluentd.

Agora que temos o Fluentd em execução, podemos enviar logs do Nginx para lá. Normalmente executamos o Nginx em um contêiner Docker; nesse caso, o Docker possui um driver de log nativo para 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

Se você executar o Nginx de maneira diferente, poderá usar arquivos de log, o Fluentd tem plugin de cauda de arquivo.

Vamos adicionar a análise de log configurada acima à configuração do Fluent:

<filter YOUR-NGINX-TAG.*>
  @type parser
  key_name log
  emit_invalid_record_to_error false
  <parse>
    @type json
  </parse>
</filter>

E enviar logs para o Kinesis usando 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>

Atena

Se você configurou tudo corretamente, depois de um tempo (por padrão, o Kinesis registra os dados recebidos uma vez a cada 10 minutos), você deverá ver os arquivos de log no S3. No menu “monitoramento” do Kinesis Firehose você pode ver quantos dados são registrados no S3, bem como erros. Não se esqueça de conceder acesso de gravação ao bucket S3 para a função Kinesis. Se o Kinesis não conseguir analisar algo, ele adicionará os erros ao mesmo bucket.

Agora você pode visualizar os dados no Athena. Vamos encontrar as solicitações mais recentes para as quais retornamos erros:

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

Verificando todos os registros para cada solicitação

Agora nossos logs foram processados ​​e armazenados no S3 em ORC, compactados e prontos para análise. O Kinesis Firehose até os organizou em diretórios para cada hora. No entanto, desde que a tabela não seja particionada, o Athena carregará dados de todos os tempos em todas as solicitações, com raras exceções. Este é um grande problema por dois motivos:

  • O volume de dados cresce constantemente, retardando as consultas;
  • O Athena é cobrado com base no volume de dados escaneados, com mínimo de 10 MB por solicitação.

Para corrigir isso, usamos o AWS Glue Crawler, que rastreará os dados no S3 e gravará as informações da partição no Glue Metastore. Isso nos permitirá usar partições como filtro ao consultar o Athena e verificará apenas os diretórios especificados na consulta.

Configurando o Amazon Glue Crawler

O Amazon Glue Crawler verifica todos os dados no bucket S3 e cria tabelas com partições. Crie um Glue Crawler no console do AWS Glue e adicione um bucket onde você armazena os dados. Você pode usar um rastreador para vários buckets; nesse caso, ele criará tabelas no banco de dados especificado com nomes que correspondam aos nomes dos buckets. Se você planeja usar esses dados regularmente, configure o cronograma de lançamento do Crawler para atender às suas necessidades. Usamos um Crawler para todas as tabelas, que é executado a cada hora.

Tabelas particionadas

Após a primeira inicialização do rastreador, as tabelas de cada bucket verificado deverão aparecer no banco de dados especificado nas configurações. Abra o console do Athena e encontre a tabela com os logs do Nginx. Vamos tentar ler 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 selecionará todos os registros recebidos entre 6h e 7h do dia 8 de abril de 2019. Mas isso é muito mais eficiente do que apenas ler uma tabela não particionada? Vamos descobrir e selecionar os mesmos registros, filtrando-os por timestamp:

Análise de log Nginx usando Amazon Athena e Cube.js

3.59 segundos e 244.34 megabytes de dados em um conjunto de dados com apenas uma semana de registros. Vamos tentar um filtro por partição:

Análise de log Nginx usando Amazon Athena e Cube.js

Um pouco mais rápido, mas o mais importante: apenas 1.23 megabytes de dados! Seria muito mais barato se não fosse pelo mínimo de 10 megabytes por solicitação no preço. Mas ainda é muito melhor e, em grandes conjuntos de dados, a diferença será muito mais impressionante.

Construindo um painel usando Cube.js

Para montar o dashboard, utilizamos o framework analítico Cube.js. Possui muitas funções, mas estamos interessados ​​em duas: a capacidade de usar filtros de partição automaticamente e pré-agregação de dados. Ele usa esquema de dados esquema de dados, escrito em Javascript para gerar SQL e executar uma consulta ao banco de dados. Precisamos apenas indicar como usar o filtro de partição no esquema de dados.

Vamos criar um novo aplicativo Cube.js. Como já estamos usando a pilha AWS, é lógico usar Lambda para implantação. Você pode usar o modelo expresso para geração se planeja hospedar o backend Cube.js no Heroku ou Docker. A documentação descreve outros métodos de hospedagem.

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

Variáveis ​​de ambiente são usadas para configurar o acesso ao banco de dados em cube.js. O gerador criará um arquivo .env no qual você pode especificar suas chaves para Atena.

Agora precisamos esquema de dados, onde indicaremos exatamente como nossos logs são armazenados. Lá você também pode especificar como calcular métricas para painéis.

No diretório schema, crie um arquivo Logs.js. Aqui está um exemplo de modelo de dados para nginx:

Código de modelo

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

Aqui estamos usando a variável FILTER_PARAMSpara gerar uma consulta SQL com um filtro de partição.

Também definimos as métricas e parâmetros que queremos exibir no painel e especificamos pré-agregações. Cube.js criará tabelas adicionais com dados pré-agregados e atualizará automaticamente os dados conforme eles chegam. Isso não apenas acelera as consultas, mas também reduz o custo de uso do Athena.

Vamos adicionar essas informações ao arquivo de esquema de dados:

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 neste modelo que é necessário pré-agregar dados para todas as métricas utilizadas, e utilizar particionamento por mês. Particionamento de pré-agregação pode acelerar significativamente a coleta e atualização de dados.

Agora podemos montar o painel!

O back-end Cube.js fornece API REST e um conjunto de bibliotecas cliente para estruturas front-end populares. Usaremos a versão React do cliente para construir o painel. Cube.js fornece apenas dados, então precisaremos de uma biblioteca de visualização - gosto dela recarrega, mas você pode usar qualquer um.

O servidor Cube.js aceita a solicitação em Formato JSON, que especifica as métricas necessárias. Por exemplo, para calcular quantos erros o Nginx deu por dia, você precisa enviar a seguinte solicitação:

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

Vamos instalar o cliente Cube.js e a biblioteca de componentes React via NPM:

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

Importamos componentes cubejs и QueryRendererpara baixar os dados e coletar o painel:

Código do painel

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

As fontes do painel estão disponíveis em caixa de areia de código.

Fonte: habr.com

Adicionar um comentário