Desduplicação de log com o Elasticsearch
Eventos duplicados de serviços de aplicação não íntegros tornam a busca de logs complicada. Confira como lidar com duplicatas usando Logstash, Beats e Elastic Agent.

Deduplicação de log com o Elasticsearch
Os SREs são inundados por grandes volumes de logs de aplicações ruidosas todos os dias. Em sua obra seminal The Mythical Man Month, Frederick P. Brooks disse que "todos os programadores são otimistas." Esse otimismo se manifesta no fato de os engenheiros de software não implementarem controles para impedir que suas aplicações enviem logs contínuos em situações de falha excepcionais. Em grandes organizações com plataformas de logging centralizadas, essa enxurrada de eventos está sendo ingerida em plataformas de logging e ocupando volumes consideráveis de armazenamento e processamento computacional. Do ponto de vista das pessoas, isso deixa os SREs sobrecarregados e sofrendo de fadiga de alertas, pois são engolidos pela onda de mensagens, um pouco como isto:
](https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/bltcb726be86c9c8601/6543831208cc0104077cd7d4/gif.gif)
Os desenvolvedores que criam softwares, incluindo microsserviços e aplicações principais, são responsáveis por garantir que não enviem eventos de log duplicados e que estejam enviando os eventos de log corretos no nível certo. No entanto, situações como o uso de solutions de terceiros ou a manutenção de serviços antigos significam que nem sempre podemos garantir que práticas de logging responsáveis tenham sido aplicadas. Mesmo se descartarmos campos desnecessários, conforme abordado neste artigo sobre a remoção de campos de eventos de log recebidos, ainda temos um problema com o armazenamento de grandes números de eventos duplicados. Aqui discutimos os desafios de identificar logs duplicados de serviços problemáticos e como fazer a deduplicação de dados usando o Elastic Beats, o Logstash e o Elastic Agent.
What is a duplicate log entry?
Before diving into the various ways of preventing these duplicates from making it into your logging platform, we need to understand what a duplicate is. In my prior life as a software engineer, I was responsible for developing and maintaining an ecosystem of vast microservices. Some had considered retry logic that, after some time, would shut the service down gracefully and trigger appropriate alerts. However, not all services are built to gracefully handle these cases.
Service misconfiguration can also contribute to event duplication. Inadvertently changing the production log level from WARN to TRACE can lead to more aggressive event volumes that have to be handled by the logging platform.
Elasticsearch automatically generates a unique ID for each document ingested unless a document contains an _id field on ingestion. Therefore, if your service is sending repeated alerts, you run the risk of having the same event stored as multiple documents with different IDs.
Another cause can be due to retry mechanisms for the tools used for log collection. A notable example is for Filebeat, where a lost connection or shutdown can cause the retry mechanism of Filebeat to resend an event until the output acknowledges receipt of the event.
Visão geral das ferramentas
Neste blog, examinaremos as ferramentas disponíveis em quatro ferramentas da Elastic:
- O Logstash é uma ferramenta de pipeline de ETL gratuita e aberta que permite a ingestão, a transformação e a saída de dados entre uma infinidade de fontes, incluindo a ingestão para e a saída do Elasticsearch. Estes exemplos serão usados para mostrar o comportamento diferente do Elasticsearch ao gerar logs com e sem um ID específico.
- Beats são uma família de agentes lightweight que nos permitem fazer a ingestão de eventos de uma determinada fonte não apenas no Elasticsearch, mas também em outras saídas, incluindo Kafka, Redis ou Logstash.
- Pipelines de ingestão permitem que transformações e enriquecimento sejam aplicados aos documentos ingeridos no Elasticsearch. É como executar a parte de
filterdo Logstash diretamente no Elasticsearch sem a necessidade de ter outro serviço em execução. Novos pipelines podem ser criados na tela Stack Management > ingestão pipelines ou via API_ingest, conforme abordado na documentação. - O Elastic Agent é um agente único que pode ser executado em seu host e enviar logs, métricas e dados de segurança de vários serviços e infraestrutura para o Elasticsearch usando as diversas integrações compatíveis. Independentemente do motivo das duplicatas, existem vários cursos de ação possíveis no ecossistema Elastic.
Ingestão sem IDs especificados
A abordagem padrão é ignorar e ingerir todos os eventos. Quando um ID não é especificado em um documento, o Elasticsearch gerará automaticamente um novo ID para cada documento que receber. Vamos pegar um exemplo simples, disponível em este repositório do GitHub, usando um servidor HTTP Express simples. O servidor, quando executado, expõe um único endpoint que retorna uma única mensagem de log:
{"event":{"transaction_id":1,"data_set":"my-logging-app"},"message":"WARN: Unable to get an interesting response"}Usando o Logstash, podemos consultar o endpoint http://locahost:3000/ a cada 60 segundos e enviar o resultado para o Elasticsearch. Nosso logstash.conf tem a aparência abaixo:
input {
http_poller {
urls => {
simple_server => "http://localhost:3000"
}
request_timeout => 60
schedule => { cron => "* * * * * UTC"}
codec => "json"
}
}
output {
elasticsearch {
cloud_id => "${ELASTIC_CLOUD_ID}"
cloud_auth => "${ELASTIC_CLOUD_AUTH}"
index => "my-logstash-index"
}
}O Logstash enviará cada evento e, sem nenhum ID no evento, o Elasticsearch gerará um novo _id campo para servir como um identificador exclusivo para cada documento:
GET my-logstash-index/_search
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 11,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "my-logstash-index",
"_id": "-j83XYsBOwNNS8Sc0Bja",
"_score": 1,
"_source": {
"@version": "1",
"event": {
"transaction_id": 1,
"original": """{"event":{"transaction_id":1,"data_set":"my-logging-app"},"message":"WARN: Unable to get an interesting response"}""",
"data_set": "my-logging-app"
},
"message": "WARN: Unable to get an interesting response",
"@timestamp": "2023-10-23T15:47:00.528205Z"
}
},
{
"_index": "my-logstash-index",
"_id": "NT84XYsBOwNNS8ScuRlO",
"_score": 1,
"_source": {
"@version": "1",
"event": {
"transaction_id": 1,
"original": """{"event":{"transaction_id":1,"data_set":"my-logging-app"},"message":"WARN: Unable to get an interesting response"}""",
"data_set": "my-logging-app"
},
"message": "WARN: Unable to get an interesting response",
"@timestamp": "2023-10-23T15:48:00.314262Z"
}
},
// Other documents omitted
]
}
}Esse comportamento é consistente para Beats, pipelines de ingestão e Elastic Agent, pois eles enviarão todos os eventos recebidos sem configuração adicional.
BYO-ID
Especificar um ID exclusivo para cada evento usando um ID existente ignora a etapa de geração de ID do Elasticsearch discutida na seção anterior. A ingestão de um documento onde esse atributo já existe resultará na verificação pelo Elasticsearch se um documento com esse ID existe no índice, e na atualização do documento caso exista. Isso resulta em uma sobrecarga, pois o índice precisa ser pesquisado para verificar se um documento com o mesmo _id existe.
Estendendo nosso exemplo do Logstash acima, especificar o valor do ID do documento no Logstash é possível ao especificar a opção document_id no plugin de saída do Elasticsearch, que seria usado para ingestão de eventos no Elasticsearch:
# http_poller configuration omitted
output {
elasticsearch {
cloud_id => "${ELASTIC_CLOUD_ID}"
cloud_auth => "${ELASTIC_CLOUD_AUTH}"
index => "my-unique-logstash-index"
document_id => "%{[event][transaction_id]}"
}
}Isso definirá o valor do campo _id para o valor de event.transaction_id. No nosso caso, isso significa que o novo documento substituirá o documento existente na ingestão, já que ambos os documentos têm um _id de 1:
GET my-unique-logstash-index/_search
{
"took": 48,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "my-unique-logstash-index",
"_id": "1",
"_score": 1,
"_source": {
"@timestamp": "2023-10-23T16:33:00.358585Z",
"message": "WARN: Unable to get an interesting response",
"@version": "1",
"event": {
"original": """{"event":{"transaction_id":1,"data_set":"my-logging-app"},"message":"WARN: Unable to get an interesting response"}""",
"data_set": "my-logging-app",
"transaction_id": 1
}
}
}
]
}
}O ID é especificado de várias maneiras, dependendo da ferramenta, conforme discutido mais detalhadamente nas seções subsequentes.
Beats
Para documentos JSON, que é um formato comum para muitas fontes de logs, se o seu evento tiver um ID útil e significativo que possa ser usado como o ID exclusivo de um documento e evitar entradas duplicadas, usar o processador decode_json_fields ou o json.document_ID configuração de entrada conforme recomendado na documentação. Essa abordagem é preferível à geração de uma chave quando uma chave natural está presente dentro de um campo JSON em nossa mensagem.
Ambas as configurações são mostradas no exemplo abaixo:
filebeat.inputs:
- type: filestream
id: my-logging-app
paths:
- /var/tmp/other.log
- /var/log/*.log
json.document_id: "event.transaction_id"
# Alternative approach using decode_json_fields processor
processors:
- decode_json_fields:
document_id: "event.transaction_id"
fields: ["message"]
max_depth: 1
target: ""Pipelines de ingestão
Nesse caso, o ID pode ser definido usando um set processor combinado com a opção copy_from para transferir o valor do seu campo único para o @metadata._id do Elasticsearch atributo:
PUT _ingest/pipeline/test-pipeline
{
"processors": [
{
"set": {
"field": "_id",
"copy_from": "transaction_id"
}
}
]
}Elastic Agent
O Elastic Agent tem uma abordagem semelhante na qual você pode usar o processador copy_fields para copiar o valor para @metadata._id atributo na integração:
- copy_fields:
fields:
- from: transaction_id
to: @metadata._id
fail_on_error: true
ignore_missing: trueA configuração fail_on_error, quando verdadeira, resultará em um retorno ao estado anterior, revertendo as alterações aplicadas pelo processador com falha. Enquanto isso, ignore_missing só acionará uma falha para um documento com um campo inexistente quando definido como false.
ID gerado automaticamente
Gerando um ID exclusivo usando técnicas como fingerprinting em um subconjunto de campos de evento. Ao aplicar hash em um conjunto de campos, um valor exclusivo é gerado que, quando correspondido, resultará em uma atualização do documento original na ingestão no Elasticsearch.
Como este artigo prático sobre como lidar com duplicatas com o Logstash descreve especificamente, o plugin de filtro de impressão digital pode ser configurado para gerar um ID com o algoritmo de hash especificado para o campo @metadata.fingerprint:
filter {
fingerprint {
source => ["event.start_date", "event.data_set", "message"]
target => "[@metadata][fingerprint]"
method => "SHA256"
}
}
output {
elasticsearch {
hosts => "my-elastic-cluster.com"
document_id => "%{[@metadata][fingerprint]}"
}
}Se não especificado, o algoritmo de hashing padrão SHA256 será usado para fazer o hash da combinação |event.start_date|start_date_value|event.data_set|data_set_value|message|message_value|. Se quiséssemos usar uma das outras opções de algoritmo permitidas, ela pode ser especificada usando a opção method. Isso resultará na atualização, pelo Elasticsearch, do documento que corresponde ao _id gerado:
GET my-fingerprinted-logstash-index/_search
{
"took": 8,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "my-fingerprinted-logstash-index",
"_id": "b2faceea91b83a610bf64ac2b12e3d3b95527dc229118d8f819cdfaa4ba98af1",
"_score": 1,
"_source": {
"@timestamp": "2023-10-23T16:46:00.772480Z",
"message": "WARN: Unable to get an interesting response",
"@version": "1",
"event": {
"original": """{"event":{"transaction_id":1,"data_set":"my-logging-app"},"message":"WARN: Unable to get an interesting response"}""",
"data_set": "my-logging-app",
"transaction_id": 1
}
}
}
]
}
}Se o seu evento não tiver um único campo de identificação significativo, esta pode ser uma opção útil se você estiver disposto a assumir a sobrecarga de processamento da geração do ID, ou o potencial de colisões onde eventos diferentes resultam no mesmo hash gerado. Capacidades semelhantes estão disponíveis para outras ferramentas, conforme discutido nas seções subsequentes.
Beats
O processador add_id para Beats e Elastic Agent permitirá que um ID exclusivo compatível com o Elasticsearch seja gerado. Por padrão, esse valor será armazenado em @metadata._id campo que é o campo de ID para documentos do Elasticsearch.
filebeat.inputs:
- type: filestream
ID: my-logging-app
paths:
- /var/tmp/other.log
- /var/log/*.log
json.document_ID: "event.transaction_id"
processors:
- add_ID: ~
target_field: @metadata._idAlternativamente, o fingerprint processor gera um valor hash de uma concatenação do nome do campo especificado e pares de valores separados pelo operador |.
filebeat.inputs:
- type: filestream
ID: my-logging-app
paths:
- /var/tmp/other.log
- /var/log/*.log
processors:
- fingerprint:
fields: ["event.start_date", "event.data_set", "message"]
target_field: "@metadata._id"
method: "sha256"
ignore_missing: falseNo exemplo acima, o algoritmo de hash padrão sha256 será usado para fazer o hash da combinação |event.start_date|start_date_value|event.data_set|data_set_value|message|message_value|. Se quisermos usar uma das outras opções de algoritmo permitidas, ela pode ser especificada usando a opção method.
O tratamento de erros também é uma consideração importante com a qual a opção ignore_missing auxilia. Por exemplo, se o event.start_date campo não existir em um determinado documento, um erro será gerado quando ignore_missing estiver definido como false. Esta é a implementação padrão se ignore_missing não for definido explicitamente, mas é comum ignorar erros especificando o valor como true.
Elastic Agent
Assim como os Beats, o Elastic Agent possui um add_id processor que pode ser usado para gerar um ID exclusivo, usando @metadata._id por padrão se o atributo target_field não for especificado:
- add_id:
target_field: "@metadata._id"Alternativamente, o fingerprint processor também está disponível no Elastic Agent e pode ser aplicado a qualquer segmento de integração que inclua uma seção de configuração avançada, incluindo uma opção de processadores. A lógica do processador tem a seguinte aparência:
- fingerprint:
fields: ["event.start_date", "event.data_set", "message"]
target_field: "@metadata._id"
ignore_missing: false
method: "sha256"Tomando o Kafka integração como exemplo, o trecho de processador acima pode ser aplicado no segmento de processador da seção de configuração avançada para Coletar logs de brokers do Kafka:
](https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt6a63c6db6d13008d/654384290970dd001bd1637d/image_2_kafka.png)
Assim como nos Beats, o valor que é submetido a hash é construído como uma concatenação do nome do campo e do valor do campo separados por |. Por exemplo |field1|value1|field2|value2|. No entanto, assim como nos Beats e ao contrário do Logstash, o valor method está em letras minúsculas, apesar de oferecer suporte aos mesmos algoritmos de codificação.
Pipelines de ingestão
Aqui mostraremos a solicitação de exemplo para criar um pipeline com um fingerprint processor com a API _ingest. Observe as semelhanças da configuração abaixo com nossos processadores Beats:
PUT _ingest/pipeline/my-logging-app-pipeline
{
"description": "Event and field dropping for my-logging-app",
"processors": [
{
"fingerprint": {
fields: ["event.start_date", "event.data_set", "message"]
target_field: "@metadata._id"
ignore_missing: false
method: "SHA-256"
}
}
]
}Agregação de eventos
Agrupar eventos com base em campos comuns é outra opção, caso a ferramenta utilizada ofereça suporte a isso. A agregação de eventos traz uma compensação, pois a ferramenta precisa manter vários eventos na memória para realizar a agregação, em vez de encaminhar o evento imediatamente para a saída. Por esse motivo, a única ferramenta dentro do ecossistema Elastic que oferece suporte à agregação de eventos é o Logstash.
Para implementar a abordagem baseada em agregação no Logstash, use o aggregate plugin. No nosso caso, é improvável que um evento final específico seja enviado para distinguir entre duplicatas, o que significa que especificar um timeout conforme o exemplo abaixo é necessário para controlar o processo de lote:
filter {
grok {
match => [ "message", %{NOTSPACE:event.start_date} "%{LOGLEVEL:loglevel} - %{NOTSPACE:user_ID} - %{GREEDYDATA:message}" ]
}
aggregate {
task_ID => "%{event.start_date}%{loglevel}%{user_ID}"
code => "map['error_count'] ||= 0; map['error_count'] += 1;"
push_map_as_event_on_timeout => true
timeout_task_ID_field => "user_id"
timeout => 600
timeout_tags => ['_aggregatetimeout']
timeout_code => "event.set('has_multiple_occurrences', event.get('error_count') > 1)"
}
}O exemplo acima enviará um evento após 600 segundos, ou 10 minutos, adicionando os atributos error_count e has_multiple_occurrences ao evento para indicar um evento de agregação. A opção push_map_as_event_on_timeout garantirá que o resultado da agregação seja enviado a cada tempo limite, permitindo que você reduza o volume de alertas. Ao determinar o tempo limite para seus dados, considere seu volume e opte pelo menor tempo limite possível, já que o Logstash manterá os eventos na memória até que o tempo limite expire e o evento de agregação seja enviado.
Conclusions
Log volume spikes can quickly overwhelm logging platforms and SRE engineers looking to maintain reliable applications. We have discussed several approaches to handling duplicate events using Elastic Beats, Logstash (which are available in this GitHub repository), and Elastic Agent.
When generating IDs via a hashing algorithm using fingerprint processors, or performing aggregates, consider the attributes used carefully to balance preventing a flood and obfuscating legitimate streams pointing to a large-scale problem in your ecosystem. Both approaches have an overhead, either in terms of processing to generate the ID, or memory overhead to store the documents eligible to aggregate.
Selecting an option really depends on the events you consider duplicates and the performance trade-offs. As discussed, when you specify an ID Elasticsearch needs to check for the existence of a document matching that ID before adding the document to the index. This results in a slight delay in ingestion to perform the _id existence check.
Using hashing algorithms to generate the ID adds additional processing time as the ID needs to be generated for each event before it is compared and potentially ingested. Choosing to not specify an ID bypasses this check as Elastic will generate the ID for you, but will result in all events being stored which increases your storage footprint.
Dropping full events is a legitimate practice not covered in this piece. If you want to drop log entries to reduce your volume check out this piece on pruning fields from incoming log events.
If your favorite way to deduplicate events is not listed here, do let us know!
Recursos
- Elastic Beats
- Filebeat | Processador de impressão digital
- Filebeat | Processador de decodificação de campos JSON
- Filebeat | Deduplicação do Filebeat
- Logstash
- Logstash | Plugin de impressão digital
- Logstash | Plugin de agregação
- Elastic Agent
- Elastic Agent | Processador de impressão digital
- Pipelines de ingestão
- Elasticsearch | Processador de impressão digital de pipeline de ingestão