Elasticsearch를 사용한 로그 중복 제거
비정상적인 애플리케이션 서비스에서 발생하는 중복 이벤트는 로그 검색을 어렵게 만듭니다. Logstash, Beats 및 Elastic Agent를 사용하여 중복을 처리하는 방법을 확인해 보십시오.

Elasticsearch를 사용한 로그 중복 제거
SRE는 매일 노이즈가 많은 애플리케이션에서 발생하는 대량의 로그로 인해 어려움을 겪습니다. 그의 기념비적인 저서 The Mythical Man Month에서, Frederick P. Brooks는 "모든 프로그래머는 낙관주의자이다."라고 말했습니다. 이러한 낙관주의는 소프트웨어 엔지니어가 예외적인 장애 상황에서 애플리케이션이 지속적으로 로그를 전송하는 것을 막는 제어 장치를 마련하지 않는 것으로 나타납니다. 중앙 집중식 로깅 플랫폼을 갖춘 대규모 조직에서는 이러한 이벤트의 홍수가 로깅 플랫폼으로 수집되어 상당한 저장 공간과 처리 컴퓨팅 자원을 차지하고 있습니다. 인적 측면에서 보면, SRE들은 메시지의 파도에 휩쓸려 압도당하고 경고로 인한 피로를 겪게 되는데, 마치 다음과 같은 상황과 비슷합니다:
](https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/bltcb726be86c9c8601/6543831208cc0104077cd7d4/gif.gif)
마이크로서비스 및 주요 애플리케이션을 포함한 소프트웨어를 구축하는 개발자는 중복된 로그 이벤트가 전송되지 않도록 하고, 올바른 수준에서 정확한 로그 이벤트를 전송하도록 보장할 책임이 있습니다. 그럼에도 불구하고, 타사 solutions을 사용하거나 노후화된 서비스를 유지 관리하는 상황에서는 책임 있는 로깅 관행이 적용되었음을 항상 보장할 수는 없습니다. 수신 로그 이벤트에서 필드를 정리하는 방법에 관한 이 글에서 다룬 것처럼 불필요한 필드를 삭제하더라도, 여전히 다량의 중복 이벤트를 저장하는 문제에 직면해 있습니다. 여기에서는 문제가 있는 서비스에서 중복 로그를 식별하는 데 따르는 어려움과 Elastic Beats, Logstash 및 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.
도구 개요
이 블로그에서는 네 가지 Elastic 도구에서 사용할 수 있는 도구들을 살펴봅니다:
- Logstash는 Elasticsearch로의 수집 및 Elasticsearch로부터의 출력을 포함하여, 수많은 소스 간에 데이터를 인제스트, 변환 및 출력할 수 있는 무료 오픈 소스 ETL 파이프라인 도구입니다. 이 예제들은 특정 ID가 있을 때와 없을 때 로그를 생성하는 Elasticsearch의 서로 다른 동작을 보여주는 데 사용됩니다.
- Beats 는 특정 소스에서 발생하는 이벤트를 Elasticsearch뿐만 아니라 Kafka, Redis 또는 Logstash를 포함한 다른 출력으로도 수집할 수 있게 해주는 경량 데이터 수집기 제품군입니다.
- 인제스트 파이프라인 Elasticsearch로 수집된 문서에 변환 및 보강을 적용할 수 있도록 합니다. 이는 다른 서비스를 실행할 필요 없이 Logstash의
filter부분을 Elasticsearch 내에서 직접 실행하는 것과 같습니다. 새 파이프라인은 Stack Management > 인제스트 파이프라인 화면 내에서 생성하거나 문서에서 다루는 바와 같이_인제스트API를 통해 생성할 수 있습니다. - Elastic Agent는 호스트에서 실행되어 지원되는 다양한 통합을 사용하여 여러 서비스 및 인프라의 로그, 메트릭 및 보안 데이터를 Elasticsearch로 전송할 수 있는 단일 에이전트입니다. 중복의 원인과 관계없이, Elastic 에코시스템에는 취할 수 있는 몇 가지 조치 방법이 있습니다.
ID가 지정되지 않은 수집
기본 접근 방식은 모든 이벤트를 무시하지 않고 수집하는 것입니다. 문서에 ID가 지정되지 않은 경우, Elasticsearch는 수신하는 각 문서에 대해 새로운 ID를 자동으로 생성합니다. 간단한 Express HTTP 서버를 사용하는 이 GitHub 리포지토리에서 제공되는 간단한 예제를 살펴보겠습니다. 서버가 실행되면 단일 로그 메시지를 반환하는 단일 엔드포인트를 노출합니다:
{"event":{"transaction_id":1,"data_set":"my-logging-app"},"message":"WARN: Unable to get an interesting response"}Logstash를 사용하여 60초마다 엔드포인트 http://locahost:3000/를 폴링하고 그 결과를 Elasticsearch로 전송할 수 있습니다. 당사의 logstash.conf 다음과 같습니다:
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"
}
}Logstash는 각 이벤트를 푸시하며, 이벤트에 ID가 없는 경우 Elasticsearch는 새로운 _id를 생성합니다. 각 문서의 고유 식별자 역할을 하는 필드:
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
]
}
}이 동작은 Beats, 인제스트 파이프라인 및 Elastic Agent에서 일관되게 나타나며, 추가 구성 없이 수신된 모든 이벤트를 전송합니다.
BYO-ID
기존 ID를 사용하여 각 이벤트에 대한 고유 ID를 지정하면 이전 섹션에서 설명한 Elasticsearch ID 생성 단계가 생략됩니다. 이 속성이 이미 존재하는 문서를 수집하면 Elasticsearch는 해당 ID를 가진 문서가 인덱스에 존재하는지 확인하고, 존재하는 경우 문서를 업데이트합니다. 동일한 _id를 가진 문서가 존재하는지 확인하기 위해 인덱스를 검색해야 하므로 오버헤드가 발생합니다.
위의 Logstash 예제를 확장하여, Logstash에서 문서 ID 값을 지정하는 것은 Elasticsearch 출력 플러그인에서 document_id 옵션을 지정함으로써 가능하며, 이는 이벤트를 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]}"
}
}이 설정은 _id 필드의 값을 event.transaction_id의 값으로 지정합니다. 이 경우, 두 문서 모두 _id가 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
}
}
}
]
}
}ID는 도구에 따라 다양한 방식으로 지정되며, 이에 대해서는 후속 섹션에서 자세히 설명합니다.
Beats
많은 로그 소스에서 공통 형식으로 사용되는 JSON 문서의 경우, 이벤트에 문서의 고유 ID로 사용하고 중복 항목을 방지할 수 있는 유용하고 의미 있는 ID가 있다면 decode_json_fields 프로세서 또는 json.document_ID를 사용하십시오. 설명서에서 권장하는 입력 설정. 이 접근 방식은 메시지 내의 JSON 필드 안에 자연 키가 있을 때 키를 생성하는 것보다 선호됩니다.
두 설정 모두 아래 예제에 나와 있습니다:
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: ""인제스트 파이프라인
이 경우, set 프로세서를 사용하여 ID를 설정할 수 있습니다 copy_from 옵션과 결합하여 고유 필드의 값을 Elasticsearch @metadata._id로 전송합니다 속성:
PUT _ingest/pipeline/test-pipeline
{
"processors": [
{
"set": {
"field": "_id",
"copy_from": "transaction_id"
}
}
]
}Elastic Agent
Elastic Agent도 유사한 접근 방식을 취하며, copy_fields 프로세서를 사용하여 값을 @metadata._id로 복사할 수 있습니다. 통합의 속성:
- copy_fields:
fields:
- from: transaction_id
to: @metadata._id
fail_on_error: true
ignore_missing: truefail_on_error 설정이 true이면 실패한 프로세서에 의해 적용된 변경 사항을 되돌려 이전 상태로 복귀합니다. 한편 ignore_missing이 false로 설정된 경우에만 존재하지 않는 필드가 있는 문서에 대해 오류가 발생합니다.
자동 생성된 ID
이벤트 필드의 하위 집합에 핑거프린팅과 같은 기술을 사용하여 고유 ID를 생성합니다. 필드 세트를 해싱하여 고유 값을 생성하며, 이 값이 일치하면 Elasticsearch로 수집될 때 원본 문서가 업데이트됩니다.
Logstash를 사용한 중복 처리에 관한 이 유용한 게시물에서 구체적으로 설명하듯이, 핑거프린트 필터 플러그인을 구성하여 지정된 해싱 알고리즘으로 ID를 생성하고 이를 @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]}"
}
}별도로 지정하지 않으면 기본 해싱 알고리즘인 SHA256이 사용되어 |event.start_date|start_date_value|event.data_set|data_set_value|message|message_value| 조합을 해싱합니다. 허용된 다른 알고리즘 옵션 중 하나를 사용하려면 method 옵션을 사용하여 지정할 수 있습니다. 그 결과 Elasticsearch는 생성된 _id와 일치하는 문서를 업데이트합니다:
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
}
}
}
]
}
}이벤트에 의미 있는 단일 식별 필드가 없는 경우, ID 생성에 따른 처리 오버헤드나 서로 다른 이벤트가 동일한 생성 해시로 확인되는 충돌 가능성을 감수할 수 있다면 이 옵션이 유용할 수 있습니다. 후속 섹션에서 논의하는 바와 같이, 다른 도구에서도 유사한 기능을 사용할 수 있습니다.
Beats
Beats 및 Elastic Agent용 add_id 프로세서를 사용하면 고유한 Elasticsearch 호환 ID를 생성할 수 있습니다. 기본적으로 이 값은 @metadata._id에 저장됩니다. Elasticsearch 문서의 ID 필드인 필드입니다.
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._id또는 fingerprint 프로세서 | 연산자로 구분된 지정된 필드 이름과 값 쌍을 연결하여 해시된 값을 생성합니다.
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: false위 예제에서는 기본 해싱 알고리즘인 sha256을 사용하여 |event.start_date|start_date_value|event.data_set|data_set_value|message|message_value| 조합을 해싱합니다. 다른 허용된 알고리즘 옵션 중 하나를 사용하려면 method 옵션을 사용하여 지정할 수 있습니다.
오류 처리 또한 ignore_missing 옵션이 지원하는 중요한 고려 사항입니다. 예를 들어, event.start_date가 지정된 문서에 필드가 존재하지 않는 경우 ignore_missing이 false로 설정되어 있으면 오류가 발생합니다. ignore_missing이 명시적으로 설정되지 않은 경우의 기본 구현이지만, 값을 true로 지정하여 오류를 무시하는 것이 일반적입니다.
Elastic Agent
Beats와 마찬가지로 Elastic Agent에는 고유 ID를 생성하는 데 사용할 수 있는 add_id 프로세서가 있으며, target_field 속성이 지정되지 않은 경우 기본값은 @metadata._id입니다:
- add_id:
target_field: "@metadata._id"또는 fingerprint 프로세서도 Elastic Agent에서 사용할 수 있으며, processors 옵션을 포함한 고급 구성 섹션이 있는 모든 통합 세그먼트에 적용할 수 있습니다. 프로세서 로직은 다음과 같습니다:
- fingerprint:
fields: ["event.start_date", "event.data_set", "message"]
target_field: "@metadata._id"
ignore_missing: false
method: "sha256"Kafka를 예로 들면 통합을 예로 들면, 위의 프로세서 스니펫은 Kafka 브로커에서 로그 수집을 위한 고급 구성 섹션의 프로세서 세그먼트에 적용할 수 있습니다:
](https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt6a63c6db6d13008d/654384290970dd001bd1637d/image_2_kafka.png)
Beats와 마찬가지로, 해시되는 값은 |로 구분된 필드 이름과 필드 값을 연결하여 구성됩니다. 예를 들어 |field1|value1|field2|value2|와 같습니다. 하지만 Beats와 마찬가지로, 그리고 Logstash와는 달리 동일한 인코딩 알고리즘을 지원함에도 불구하고 method 값은 소문자로 표시됩니다.
인제스트 파이프라인
여기서는 _인제스트 API를 사용하여 fingerprint 프로세서로 파이프라인을 생성하는 샘플 요청을 보여드리겠습니다. 아래 구성이 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"
}
}
]
}이벤트 집계
사용 중인 도구가 지원한다면 공통 필드를 기반으로 이벤트를 함께 집계하는 것도 또 다른 방법입니다. 이벤트 집계에는 트레이드오프가 따릅니다. 도구가 이벤트를 즉시 출력으로 전달하는 대신 집계를 수행하기 위해 여러 이벤트를 메모리에 유지해야 하기 때문입니다. 이러한 이유로 Elastic 생태계 내에서 이벤트 집계를 지원하는 유일한 도구는 Logstash입니다.
Logstash에서 집계 기반 접근 방식을 구현하려면 aggregate 플러그인을 사용하십시오. 저희의 경우 중복을 구분하기 위해 특정 종료 이벤트가 전송될 가능성은 낮으므로, 배치 프로세스를 제어하려면 아래 예시와 같이 timeout을 지정해야 합니다:
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)"
}
}위의 예시는 600초(10분) 후에 이벤트를 전송하며, 집계된 이벤트임을 나타내기 위해 error_count 및 has_multiple_occurrences 속성을 이벤트에 추가합니다. push_map_as_event_on_timeout 옵션은 각 타임아웃 시 집계 결과가 푸시되도록 보장하여 경보 볼륨을 줄일 수 있게 합니다. 데이터의 타임아웃을 결정할 때는 볼륨을 고려하고 가능한 가장 낮은 타임아웃을 선택하십시오. Logstash는 타임아웃이 만료되고 집계된 이벤트가 푸시될 때까지 이벤트를 메모리에 보관하기 때문입니다.
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!