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
TL: DR;
Para coletar informações que usamos
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”:
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
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.
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
Primeiro, precisamos do arquivo de configuração fluent.conf. Crie-o e adicione a fonte:
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
$ 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
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
<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:
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:
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
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
$ 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
Agora precisamos
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
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.
Agora podemos montar o painel!
O back-end Cube.js fornece
O servidor Cube.js aceita a solicitação em
{
"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
и QueryRenderer
para 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
Fonte: habr.com