Guance 로그 수집 및 분석 모범 사례¶
로그 수집¶
먼저, 로그란 무엇일까요? 로그는 프로그램이 생성하며, 일정한 형식(일반적으로 타임스탬프 포함)을 따르는 텍스트 데이터입니다.
일반적으로 로그는 서버에서 생성되어 여러 파일에 출력되며, 시스템 로그, 애플리케이션 로그, 보안 로그 등이 있습니다. 이러한 로그는 여러 머신에 분산되어 저장됩니다. 시스템에 장애가 발생하면 엔지니어는 각 서버에 로그인하여 grep / sed / awk 등의 Linux 스크립트 도구를 사용해 로그에서 장애 원인을 찾아야 합니다. 로그 시스템이 없는 경우, 먼저 요청을 처리하는 서버를 찾아야 하며, 이 서버에 여러 인스턴스가 배포되어 있다면 각 애플리케이션 인스턴스의 로그 디렉터리에서 로그 파일을 찾아야 합니다. 또한 각 애플리케이션 인스턴스는 로그 롤링 정책(예: 매일 하나의 파일 생성 또는 로그 파일이 특정 크기에 도달하면 새 파일 생성)과 로그 압축 및 보관 정책 등을 설정합니다.
이러한 일련의 과정은 장애를 조사하고 원인을 신속히 찾는 데 상당한 어려움을 초래합니다. 따라서 이러한 로그를 중앙에서 관리하고 집중 검색 기능을 제공할 수 있다면 진단 효율성을 높일 수 있을 뿐만 아니라 시스템 상황을 종합적으로 이해하여 사후 대응의 수동적 입장을 피할 수 있습니다.
그러므로 로그 데이터는 다음과 같은 측면에서 매우 중요한 역할을 합니다:
- 데이터 검색: 로그 정보를 검색하여 해당 버그를 찾고 해결 방안을 도출합니다.
- 서비스 진단: 로그 정보를 통계 및 분석하여 서버의 부하와 서비스 실행 상태를 파악합니다.
- 데이터 분석: 추가적인 데이터 분석을 수행할 수 있습니다.
파일 로그 수집¶
본 문서에서는 Nginx 로그 수집을 예시로 설명합니다. DataKit 설치 디렉터리의 conf.d/log 디렉터리로 이동하여 logging.conf.sample을 복사하고 logging.conf로 이름을 변경합니다. 예시는 다음과 같습니다.
[[inputs.logging]]
# 로그 파일 목록. 절대 경로를 지정할 수 있으며 glob 규칙을 사용하여 일괄 지정 가능
# 절대 경로 사용 권장
logfiles = [
"/var/log/nginx/access.log",
"/var/log/nginx/error.log",
]
# 파일 경로 필터링. glob 규칙 사용. 필터 조건 중 하나라도 일치하면 해당 파일을 수집하지 않음
ignore = [""]
# 데이터 소스. 비어 있으면 기본값 'default' 사용
source = ""
# 추가 태그. 비어 있으면 기본값 $source 사용
service = ""
# pipeline 스크립트 경로. 비어 있으면 $source.p 사용. $source.p가 없으면 pipeline 사용 안 함
pipeline = "nginx.p"
# status 필터링:
# `emerg`,`alert`,`critical`,`error`,`warning`,`info`,`debug`,`OK`
ignore_status = []
# 인코딩 선택. 인코딩이 잘못되면 데이터를 볼 수 없음. 기본값은 비워 둠:
# `utf-8`, `utf-16le`, `utf-16le`, `gbk`, `gb18030` or ""
character_encoding = ""
## 정규 표현식 설정. 예: ^\d{4}-\d{2}-\d{2} 행 시작 부분이 YYYY-MM-DD 시간 형식과 일치
## 이 정규식과 일치하는 데이터는 유효한 데이터로 간주되며, 그렇지 않으면 이전 유효 데이터 끝에 누적 추가됨
## 이스케이프 방지를 위해 3개의 작은따옴표 '''this-regexp''' 사용
## 정규 표현식 링크: https://golang.org/pkg/regexp/syntax/#hdr-Syntax
# multiline_match = '''^\S'''
## ANSI 이스케이프 코드(예: 표준 출력의 텍스트 색상 등) 삭제 여부
remove_ansi_escape_codes = false
# 사용자 정의 tags
[inputs.logging.tags]
app = oa
사용자 정의 tags는 사용자 정의 태그이며, key-value 값을 자유롭게 입력할 수 있습니다.
● 구성 완료 후 모든 메트릭에 app = oa 태그가 포함되어 빠른 쿼리가 가능합니다.
● 관련 문서: < DataFlux Tag 적용 모범 사례>
DataKit 재시작
멀티라인 로그 수집¶
멀티라인 로그의 첫 번째 줄 특징을 식별하여 특정 로그 줄이 새로운 로그인지 판단할 수 있습니다. 이 특징과 일치하지 않으면 현재 로그 줄은 이전 멀티라인 로그의 추가일 뿐이라고 간주합니다.
예를 들어, 일반적으로 로그는 처음부터 작성되지만 일부 로그 텍스트는 그렇지 않습니다. 예를 들어 프로그램 충돌 시의 호출 스택 로그가 있습니다. 이러한 로그 텍스트는 멀티라인 로그입니다. DataKit에서는 정규 표현식을 사용하여 멀티라인 로그 특징을 식별합니다. 정규식과 일치하는 로그 줄은 새 로그의 시작이며, 이후 일치하지 않는 모든 로그 줄은 이 새 로그의 추가로 간주되며, 정규식과 일치하는 다른 새 로그 줄을 만날 때까지 계속됩니다.
다음과 같이 멀티라인 로그 수집을 활성화하려면 logging.conf에서 구성을 수정해야 합니다.
로그 수집기에 사용되는 정규 표현식 스타일은 참조
다음은 Python 로그 예시입니다.
2020-10-23 06:41:56,688 INFO demo.py 1.0
2020-10-23 06:54:20,164 ERROR /usr/local/lib/python3.6/dist-packages/flask/app.py Exception on /0 [GET]
Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/flask/app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
ZeroDivisionError: division by zero
2020-10-23 06:41:56,688 INFO demo.py 5.0
Match 구성: ^\d{4}-\d{2}-\d{2}.* (즉, 2020-10-23과 같은 행 시작 부분 일치)
로그 특수 바이트코드 필터링¶
로그에는 읽을 수 없는 바이트코드(예: 터미널 출력 색상 등)가 포함될 수 있습니다. logging.conf에서 remove_ansi_escape_codes를 true로 설정하여 삭제 및 필터링할 수 있습니다.
이 기능을 활성화하면 처리 시간이 약간 증가합니다.
원격 로그 파일 수집¶
Linux 시스템에서는 NFS 방식을 통해 로그가 있는 호스트의 파일 경로를 DataKit이 설치된 호스트에 마운트한 후, Logging 수집기에서 해당 로그 경로를 구성하여 수집을 완료할 수 있습니다.
스트리밍 로그 수집¶
본 문서에서는 Fluentd 로그 수집을 예시로 설명합니다.
예시 Fluentd 버전: td-agent-4.2.x, 버전별로 구성이 다를 수 있습니다.
DataKit 구성¶
스트리밍 로그 수집 시 DataKit은 HTTP Server를 시작하여 로그 텍스트 데이터를 수신하고 Guance에 업로드합니다. HTTP URL은 고정: /v1/write/logstreaming, 즉 http://Datakit_IP:PORT/v1/write/logstreaming
참고: DataKit이 Kubernetes에 daemonset으로 배포된 경우 Service 방식으로 접근할 수 있으며 주소는
http://datakit-service.datakit:9529입니다.
DataKit 설치 디렉터리의 conf.d/log 디렉터리로 이동하여 logstreaming.conf.sample을 복사하고 logstreaming.conf로 이름을 변경합니다. 예시는 다음과 같습니다.
DataKit 재시작
매개변수 지원¶
Logstreaming은 HTTP URL에 매개변수를 추가하여 로그 데이터를 조작하는 것을 지원합니다. 매개변수 목록은 다음과 같습니다.
type: 데이터 형식. 현재는influxdb만 지원합니다.- type이 influxdb인 경우(
/v1/write/logstreaming?type=influxdb), 데이터 자체가 라인 프로토콜 형식(기본 precision은 s)이므로 내장 Tags만 추가하고 다른 작업은 수행하지 않습니다. - 이 값이 비어 있으면 데이터에 대해 행 분할 및 pipeline 처리 등을 수행합니다.
source: 데이터 소스를 식별합니다. 즉 라인 프로토콜의 measurement입니다. 예: nginx 또는 redis (/v1/write/logstreaming?source=nginx)type이influxdb인 경우 이 값은 유효하지 않습니다.- 기본값은
default입니다. service: service 태그 필드를 추가합니다. 예: (/v1/write/logstreaming?service=nginx_service)- 기본값은 source 매개변수 값입니다.
pipeline: 데이터에 사용할 pipeline 이름을 지정합니다. 예:nginx.p(/v1/write/logstreaming?pipeline=nginx.p)
Fluentd 구성¶
Fluentd가 Nginx 로그를 수집하여 상위 Server로 전달하는 Plugin 구성을 예로 들어 보겠습니다. Server로 직접 보내 처리하지 않고, DataKit으로 보내 Guance 플랫폼에서 분석하려고 합니다.
##pc端日志收集
<source>
@type tail
format ltsv
path /var/log/nginx/access.log
pos_file /var/log/buffer/posfile/access.log.pos
tag nginx
time_key time
time_format %d/%b/%Y:%H:%M:%S %z
</source>
## 收集的数据由tcp协议转发到多个server的49875端口
<match nginx>
type forward
<server>
name es01
host es01
port 49875
weight 60
</server>
<server>
name es02
host es02
port 49875
weight 60
</server>
</match>
##pc端日志收集
<source>
@type tail
format ltsv
path /var/log/nginx/access.log
pos_file /var/log/buffer/posfile/access.log.pos
tag nginx
time_key time
time_format %d/%b/%Y:%H:%M:%S %z
</source>
##收集的数据由http协议转发至本地 DataKit
## nginx output
<match nginx>
@type http
endpoint http://127.0.0.1:9529/v1/write/logstreaming?source=nginx_td&pipeline=nginx.p
open_timeout 2
<format>
@type json
</format>
</match>
구성을 수정한 후 td-agent를 재시작하여 데이터 업로드를 완료합니다.
DQL을 통해 업로드된 데이터를 확인할 수 있습니다.
dql > L::nginx_td LIMIT 1
-----------------[ r1.nginx_td.s1 ]-----------------
__docid 'L_c6et7vk5jjqulpr6osa0'
create_time 1637733374609
date_ns 96184
host 'df-solution-ecs-018'
message '{"120.253.192.179 - - [24/Nov/2021":"13:55:10 +0800] \"GET / HTTP/1.1\" 304 0 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36\" \"-\""}'
source 'nginx_td'
time 2021-11-24 13:56:06 +0800 CST
---------
1 rows, 1 series, cost 2ms
로그 파싱 (Pipeline)¶
일반적으로 시스템이나 서비스에서 생성되는 로그는 긴 문자열 하나입니다. 각 필드는 공백으로 구분됩니다. 일반적으로 로그를 가져올 때 전체 문자열을 가져오지만, 로그의 각 필드가 의미하는 바를 분리하여 분석하면 데이터가 더 명확하게 표시되고 시각화에도 더 편리합니다.
Pipeline은 Guance에서 텍스트 데이터를 처리하는 중요한 구성 요소입니다. 주요 기능은 텍스트 형식의 문자열을 구체적인 구조화된 데이터로 변환하는 것이며, Grok 정규 표현식과 함께 사용됩니다.
Grok 패턴 분류¶
DataKit의 grok 패턴은 전역 패턴과 로컬 패턴의 두 가지로 분류할 수 있습니다. pattern 디렉터리의 패턴 파일은 모두 전역 패턴이며 모든 Pipeline 스크립트에서 사용할 수 있습니다. 반면 Pipeline 스크립트에서 add_pattern() 함수를 통해 추가된 패턴은 로컬 패턴에 속하며 현재 Pipeline 스크립트에만 유효합니다.
DataKit 내장 패턴으로 모든 사용자 요구를 충족할 수 없는 경우 사용자는 Pipeline 디렉터리에 패턴 파일을 직접 추가하여 확장할 수 있습니다. 사용자 정의 패턴이 전역 수준인 경우 pattern 디렉터리에 새 파일을 만들고 패턴을 추가해야 합니다. 기존 내장 패턴 파일에 추가하거나 수정하지 마십시오. DataKit 시작 시 내장 패턴 파일이 덮어쓰기되기 때문입니다.
로컬 패턴 추가¶
grok의 본질은 일부 정규 표현식을 미리 정의하여 텍스트 매칭 및 추출을 수행하고, 미리 정의된 정규 표현식에 이름을 지정하여 편리하게 사용하고 중첩 참조를 통해 무수히 많은 새 패턴을 확장할 수 있도록 하는 것입니다. 예를 들어 DataKit에는 다음과 같은 3개의 내장 패턴이 있습니다.
_second (?:(?:[0-5]?[0-9]|60)(?:[:.,][0-9]+)?) #秒数 매칭, _second는 패턴 이름
_minute (?:[0-5][0-9]) #分钟数 매칭, _minute는 패턴 이름
_hour (?:2[0123]|[01]?[0-9]) #年份 매칭, _hour는 패턴 이름
위의 세 가지 내장 패턴을 기반으로 time이라는 자체 내장 패턴을 확장할 수 있습니다.
# pattern 디렉터리 파일에 time을 추가합니다. 이 패턴은 전역 패턴이며 어디에서나 time을 참조할 수 있습니다.
time ([^0-9]?)%{hour:hour}:%{minute:minute}(?::%{second:second})([^0-9]?)
# add_pattern()을 통해 pipeline 파일에 추가할 수도 있습니다. 이 경우 이 패턴은 로컬 패턴이 되어 현재 pipeline 스크립트에서만 time을 사용할 수 있습니다.
add_pattern(time, "([^0-9]?)%{HOUR:hour}:%{MINUTE:minute}(?::%{SECOND:second})([^0-9]?)")
# grok을 통해 원본 입력에서 시간 필드를 추출합니다. 입력이 12:30:59라고 가정하면 {"hour": 12, "minute": 30, "second": 59}를 추출합니다.
grok(_, %{time})
참고: - 동일한 패턴 이름은 스크립트 수준이 우선합니다(즉, 로컬 패턴이 전역 패턴을 덮어씁니다). - pipeline 스크립트에서
add_pattern()은grok()함수보다 먼저 호출해야 합니다. 그렇지 않으면 첫 번째 데이터 추출이 실패할 수 있습니다.
Nginx 로그 파싱 구성¶
pipeline 파일 작성¶
<datakit安装目录>/pipeline 디렉터리에 Pipeline 파일을 작성합니다. 파일 이름은 nginx.p입니다.
add_pattern("date2", "%{YEAR}[./]%{MONTHNUM}[./]%{MONTHDAY} %{TIME}")
grok(_, "%{IPORHOST:client_ip} %{NOTSPACE:http_ident} %{NOTSPACE:http_auth} \\[%{HTTPDATE:time}\\] \"%{DATA:http_method} %{GREEDYDATA:http_url} HTTP/%{NUMBER:http_version}\" %{INT:status_code} %{INT:bytes}")
# access log
add_pattern("access_common", "%{IPORHOST:client_ip} %{NOTSPACE:http_ident} %{NOTSPACE:http_auth} \\[%{HTTPDATE:time}\\] \"%{DATA:http_method} %{GREEDYDATA:http_url} HTTP/%{NUMBER:http_version}\" %{INT:status_code} %{INT:bytes}")
grok(_, '%{access_common} "%{NOTSPACE:referrer}" "%{GREEDYDATA:agent}')
user_agent(agent)
# error log
grok(_, "%{date2:time} \\[%{LOGLEVEL:status}\\] %{GREEDYDATA:msg}, client: %{IPORHOST:client_ip}, server: %{IPORHOST:server}, request: \"%{DATA:http_method} %{GREEDYDATA:http_url} HTTP/%{NUMBER:http_version}\", (upstream: \"%{GREEDYDATA:upstream}\", )?host: \"%{IPORHOST:ip_or_host}\"")
grok(_, "%{date2:time} \\[%{LOGLEVEL:status}\\] %{GREEDYDATA:msg}, client: %{IPORHOST:client_ip}, server: %{IPORHOST:server}, request: \"%{GREEDYDATA:http_method} %{GREEDYDATA:http_url} HTTP/%{NUMBER:http_version}\", host: \"%{IPORHOST:ip_or_host}\"")
grok(_,"%{date2:time} \\[%{LOGLEVEL:status}\\] %{GREEDYDATA:msg}")
group_in(status, ["warn", "notice"], "warning")
group_in(status, ["error", "crit", "alert", "emerg"], "error")
cast(status_code, "int")
cast(bytes, "int")
group_between(status_code, [200,299], "OK", status)
group_between(status_code, [300,399], "notice", status)
group_between(status_code, [400,499], "warning", status)
group_between(status_code, [500,599], "error", status)
nullif(http_ident, "-")
nullif(http_auth, "-")
nullif(upstream, "")
default_time(time)
참고: 파싱 과정에서 tag key와의 중복 문제(Pipeline 필드 명명 주의사항)를 피해야 합니다.
Pipeline 파일 디버깅¶
Grok Pattern이 많아 수동 매칭이 번거롭습니다. DataKit은 대화형 명령줄 도구 grokq(Grok Query)를 제공합니다.
datakit --grokq
grokq > Mon Jan 25 19:41:17 CST 2021 # 여기에 매칭하려는 텍스트 입력
2 %{DATESTAMP_OTHER: ?} # 도구가 해당 제안을 제공합니다. 앞쪽일수록 매칭이 더 정확합니다(가중치도 더 큼). 앞의 숫자는 가중치를 나타냅니다.
0 %{GREEDYDATA: ?}
grokq > 2021-01-25T18:37:22.016+0800
4 %{TIMESTAMP_ISO8601: ?} # 여기의 ?는 매칭된 텍스트의 이름을 지정하는 필드가 필요함을 나타냅니다.
0 %{NOTSPACE: ?}
0 %{PROG: ?}
0 %{SYSLOGPROG: ?}
0 %{GREEDYDATA: ?} # GREEDYDATA와 같이 범위가 넓은 패턴은 가중치가 낮습니다.
# 가중치가 높을수록 매칭 정확도가 높아집니다.
grokq > Q # Q 또는 exit 입력 시 종료
Bye!
DataKit이 제공하는 명령줄 도구 grokq를 사용하여 Pipeline 파일을 작성한 후, Pipeline 스크립트 이름(--pl, Pipeline 스크립트는
#추출 성공 예시
datakit --pl nginx.p --txt '172.17.0.1 - - [06/Jan/2017:16:16:37 +0000] "GET /datadoghq/company?test=var1%20Pl HTTP/1.1" 401 612 "http://www.perdu.com/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36" "-"'
Extracted data(cost: 5.279203ms): # 파싱 성공을 의미
{
"agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\" \"-\"",
"browser": "Chrome",
"browserVer": "55.0.2883.87",
"bytes": 612,
"client_ip": "172.17.0.1",
"engine": "AppleWebKit",
"engineVer": "537.36",
"http_method": "GET",
"http_url": "/datadoghq/company?test=var1%20Pl",
"http_version": "1.1",
"isBot": false,
"isMobile": false,
"message": "172.17.0.1 - - [06/Jan/2017:16:16:37 +0000] \"GET /datadoghq/company?test=var1%20Pl HTTP/1.1\" 401 612 \"http://www.perdu.com/\" \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\" \"-\"",
"os": "Linux x86_64",
"referrer": "http://www.perdu.com/",
"status": "warning",
"status_code": 401,
"time": 1483719397000000000,
"ua": "X11"
}
# 추출 실패 예시
datakit --pl nginx.p --txt '172.17.0.1 - - [06/Jan/2017:16:16:37 +0000] "GET /datadoghq/company?test=var1%20Pl HTTP/1.1" 401 612 "http://www.perdu.com/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36" "-"'
No data extracted from pipeline
수집기에 Pipeline 스크립트 적용 구성¶
텍스트 로그 수집 Pipeline 스크립트 구성¶
Nginx 로그 수집을 예로 들어, Logging 수집기에서 Pipeline 필드를 구성하면 됩니다. 여기서는 Pipeline의 스크립트 이름을 구성하며, 경로가 아닙니다. 여기서 참조하는 모든 Pipeline 스크립트는
[inputs.logging]]
# 로그 파일 목록. 절대 경로를 지정할 수 있으며 glob 규칙을 사용하여 일괄 지정 가능
# 절대 경로 사용 권장
logfiles = [
"/var/log/nginx/access.log",
"/var/log/nginx/error.log",
]
# 파일 경로 필터링. glob 규칙 사용. 필터 조건 중 하나라도 일치하면 해당 파일을 수집하지 않음
ignore = [""]
# 데이터 소스. 비어 있으면 기본값 'default' 사용
source = ""
# 추가 태그. 비어 있으면 기본값 $source 사용
service = ""
# pipeline 스크립트 경로. 비어 있으면 $source.p 사용. $source.p가 없으면 pipeline 사용 안 함
pipeline = "nginx.p"
# status 필터링:
# `emerg`,`alert`,`critical`,`error`,`warning`,`info`,`debug`,`OK`
ignore_status = []
# 인코딩 선택. 인코딩이 잘못되면 데이터를 볼 수 없음. 기본값은 비워 둠:
# `utf-8`, `utf-16le`, `utf-16le`, `gbk`, `gb18030` or ""
character_encoding = ""
## 정규 표현식 설정. 예: ^\d{4}-\d{2}-\d{2} 행 시작 부분이 YYYY-MM-DD 시간 형식과 일치
## 이 정규식과 일치하는 데이터는 유효한 데이터로 간주되며, 그렇지 않으면 이전 유효 데이터 끝에 누적 추가됨
## 이스케이프 방지를 위해 3개의 작은따옴표 '''this-regexp''' 사용
## 정규 표현식 링크: https://golang.org/pkg/regexp/syntax/#hdr-Syntax
# multiline_match = '''^\S'''
## ANSI 이스케이프 코드(예: 표준 출력의 텍스트 색상 등) 삭제 여부
remove_ansi_escape_codes = false
# 사용자 정의 tags
[inputs.logging.tags]
app = oa
DataKit을 재시작하면 해당 로그를 파싱할 수 있습니다.
스트리밍 로그 수집 Pipeline 스크립트 구성¶
Fluentd 로그 수집을 예로 들어, Match의 Output을 수정하여 유형을 Http로 지정하고 Endpoint를 Logstreaming이 활성화된 DataKit 주소로 지정하고 Pipeline 스크립트 이름을 구성하면 수집이 완료됩니다.
##pc端日志收集
<source>
@type tail
format ltsv
path /var/log/nginx/access.log
pos_file /var/log/buffer/posfile/access.log.pos
tag nginx
time_key time
time_format %d/%b/%Y:%H:%M:%S %z
</source>
##收集的数据由http协议转发至本地 DataKit
## nginx output
<match nginx>
@type http
endpoint http://127.0.0.1:9529/v1/write/logstreaming?source=nginx_td&pipeline=nginx.p
open_timeout 2
<format>
@type json
</format>
</match>
로그 수집 성능 최적화¶
Pipeline 실행 속도가 느린 이유¶
성능 문제는 일반적으로 논의됩니다. 사용자는 Grok 표현식을 사용한 후 Pipeline이 로그를 처리하는 속도가 매우 느려지는 것을 발견합니다. Grok 패턴은 정규 표현식을 기반으로 하며, Pipeline을 작성할 때 사용하는 Grok 변수가 다루는 시나리오가 너무 많거나, 모든 행을 전체 매칭하여 처리 속도가 느려질 수 있습니다.
두 번 매칭되는 표현식 주의¶
동일한 게이트웨이에서 처리되는 여러 애플리케이션 로그(예: Syslog)에서 Grok 패턴을 사용할 때 많은 문제가 발생합니다. 다음과 같은 시나리오를 상상해 보세요. "common_header: payload" 로그 형식을 사용하여 세 가지 애플리케이션 로그를 기록했다고 가정합니다.
Application 1: '8.8.8.8 process-name[666]: a b 1 2 a lot of text at the end'
Application 2: '8.8.8.8 process-name[667]: a 1 2 3 a lot of text near the end;4'
Application 3: '8.8.8.8 process-name[421]: a completely different format | 1111'
grok(_ , "%{IPORHOST:clientip} %{DATA:process_name}\[%{NUMBER:process_id}\]: %{WORD:word_1} %{WORD:word_2} %{NUMBER:number_1} %{NUMBER:number_2} %{DATA:data}")
grok(_ , "%{IPORHOST:clientip} %{DATA:process_name}\[%{NUMBER:process_id}\]: %{WORD:word_1} %{NUMBER:number_1} %{NUMBER:number_2} %{NUMBER:number_3} %{DATA:data};%{NUMBER:number_4}")
grok(_ , "%{IPORHOST:clientip} %{DATA:process_name}\[%{NUMBER:process_id}\]: %{DATA:data} | %{NUMBER:number}")
add_pattern("message", "%{IPORHOST:clientip} %{DATA:process_name}\[%{NUMBER:process_id}\]: %{GREEDYDATA:message}")
grok(_, "%{message} %{WORD:word_1} %{WORD:word_2} %{NUMBER:number_1} %{NUMBER:number_2} %{GREEDYDATA:data}")
grok(_, "%{message} %{WORD:word_1} %{NUMBER:number_1} %{NUMBER:number_2} %{NUMBER:number_3} %{DATA:data};%{NUMBER:number_4}")
grok(_, "%{message} %{DATA:data} | %{NUMBER:number}")
고성능 Grok 표현식 주의¶
다음 Nginx 로그를 살펴보겠습니다.
172.17.0.1 - - [06/Jan/2017:16:16:37 +0000] "GET /datadoghq/company?test=var1%20Pl HTTP/1.1" 401 612 "http://www.perdu.com/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36" "-"
grok(_, "%{IPORHOST:client_ip} %{NOTSPACE:http_ident} %{NOTSPACE:http_auth} \\[%{HTTPDATE:time}\\] \"%{DATA:http_method} %{GREEDYDATA:http_url} HTTP/%{NUMBER:http_version}\" %{INT:status_code} %{INT:bytes}")
cast(status_code, "int")
cast(bytes, "int")
여기서 %{IPORHOST:client_ip} --> 172.17.0.1의 성능 오버헤드는 상당히 큽니다. Grok은 내부적으로 정규 표현식으로 변환되기 때문에, 더 많은 시나리오를 다루는 Grok 표현식일수록 성능이 저하될 수 있습니다. %{IPORHOST:client_ip}의 내부 복잡한 정규 표현식을 살펴보겠습니다.
IPORHOST (?:%{IP}|%{HOSTNAME})
HOSTNAME \b(?:[0-9A-Za-z][0-9A-Za-z-]{0,62})(?:\.(?:[0-9A-Za-z][0-9A-Za-z-]{0,62}))*(\.?|\b)
IP (?:%{IPV6}|%{IPV4})
IPV6 ((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?
IPV4 (?<![0-9])(?:(?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5]))(?![0-9])
짧은 Grok 표현식 하나에 이렇게 많은 복잡한 정규 표현식이 포함될 수 있습니다. 따라서 처리해야 할 로그량이 매우 많을 때 이렇게 복잡한 Grok 표현식을 사용하는 것이 성능에 얼마나 영향을 미치는지 알 수 있습니다. 어떻게 최적화할 수 있을까요?
grok(_, "%{NOTSPACE:client_ip} %{NOTSPACE:http_ident} %{NOTSPACE:http_auth} \\[%{HTTPDATE:time}\\] \"%{DATA:http_method} %{GREEDYDATA:http_url} HTTP/%{NUMBER:http_version}\" %{INT:status_code} %{INT:bytes}")
cast(status_code, "int")
cast(bytes, "int")
default_time(time)
성능을 중시한다면 가능한 %{NOTSPACE:}를 사용하세요. Grok은 내부적으로 정규 표현식으로 변환되므로, 더 많은 시나리오를 다루는 Grok 표현식일수록 성능이 저하될 수 있습니다. 반대로 %{NOTSPACE:}(공백 아님)와 같이 매칭이 매우 간단한 변수는 성능이 높습니다. 따라서 단어를 분리할 때 데이터가 공백이 아니고 데이터 바로 옆에 공백 문자가 있다면 %{NOTSPACE:}를 선택하여 Pipeline 성능을 향상시키는 것이 좋습니다.
도구를 활용한 Pipeline 작성¶
DataKit - 대화형 명령줄 도구 grokq¶
Grok Pattern이 많아 수동 매칭이 번거롭습니다. DataKit은 대화형 명령줄 도구 grokq(Grok Query)를 제공합니다.
datakit --grokq
grokq > Mon Jan 25 19:41:17 CST 2021 # 여기에 매칭하려는 텍스트 입력
2 %{DATESTAMP_OTHER: ?} # 도구가 해당 제안을 제공합니다. 앞쪽일수록 매칭이 더 정확합니다(가중치도 더 큼). 앞의 숫자는 가중치를 나타냅니다.
0 %{GREEDYDATA: ?}
grokq > 2021-01-25T18:37:22.016+0800
4 %{TIMESTAMP_ISO8601: ?} # 여기의 ?는 매칭된 텍스트의 이름을 지정하는 필드가 필요함을 나타냅니다.
0 %{NOTSPACE: ?}
0 %{PROG: ?}
0 %{SYSLOGPROG: ?}
0 %{GREEDYDATA: ?} # GREEDYDATA와 같이 범위가 넓은 패턴은 가중치가 낮습니다.
# 가중치가 높을수록 매칭 정확도가 높아집니다.
grokq > Q # Q 또는 exit 입력 시 종료
Bye!
DataKit - Pipeline 스크립트 테스트¶
DataKit이 제공하는 명령줄 도구 grokq를 사용하여 Pipeline 파일을 작성한 후, Pipeline 스크립트 이름(--pl, Pipeline 스크립트는
#추출 성공 예시
datakit --pl nginx.p --txt '172.17.0.1 - - [06/Jan/2017:16:16:37 +0000] "GET /datadoghq/company?test=var1%20Pl HTTP/1.1" 401 612 "http://www.perdu.com/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36" "-"'
Extracted data(cost: 5.279203ms): # 파싱 성공을 의미
{
"agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\" \"-\"",
"browser": "Chrome",
"browserVer": "55.0.2883.87",
"bytes": 612,
"client_ip": "172.17.0.1",
"engine": "AppleWebKit",
"engineVer": "537.36",
"http_method": "GET",
"http_url": "/datadoghq/company?test=var1%20Pl",
"http_version": "1.1",
"isBot": false,
"isMobile": false,
"message": "172.17.0.1 - - [06/Jan/2017:16:16:37 +0000] \"GET /datadoghq/company?test=var1%20Pl HTTP/1.1\" 401 612 \"http://www.perdu.com/\" \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\" \"-\"",
"os": "Linux x86_64",
"referrer": "http://www.perdu.com/",
"status": "warning",
"status_code": 401,
"time": 1483719397000000000,
"ua": "X11"
}
# 추출 실패 예시
datakit --pl nginx.p --txt '172.17.0.1 - - [06/Jan/2017:16:16:37 +0000] "GET /datadoghq/company?test=var1%20Pl HTTP/1.1" 401 612 "http://www.perdu.com/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36" "-"'
No data extracted from pipeline
온라인 Grok Debug¶
GrokDebug 웹사이트를 사용하여 Grok을 디버그할 수 있습니다.
로그 수집 비용 최적화¶
Guance 제품 측면에서의 비용 최적화¶
"Guance"은 로그 블랙리스트를 설정하여 조건에 맞는 로그를 필터링하는 것을 지원합니다. 즉, 로그 블랙리스트를 구성하면 조건에 맞는 로그 데이터가 더 이상 "Guance" 워크스페이스에 업로드되지 않아 사용자의 로그 데이터 저장 비용을 절약할 수 있습니다.
참고: 여기서의 구성은 DataKit에 배포 방식으로 전달되지 않습니다. 여기서의 구성은 DataKit이 센터의 구성 파일을 Get 요청하여 읽어온 후 로컬에서 필터링 작업을 수행하는 방식으로 적용됩니다.
새 로그 블랙리스트 생성¶
"Guance" 워크스페이스에서 「로그」-「블랙리스트」-「블랙리스트 생성」을 클릭하고 「로그 소스」를 선택한 후 하나 이상의 로그 필터링 규칙을 추가하고 확인을 클릭하면 해당 로그 필터링 규칙이 기본적으로 활성화됩니다. 「로그 블랙리스트」를 통해 모든 로그 필터링 규칙을 확인할 수 있습니다.

참고: 로그 필터링 조건은 "and(그리고)" 관계입니다. 즉, 필터링 조건을 동시에 충족하는 로그 데이터는 워크스페이스에 업로드되지 않습니다.
스트리밍 로그 수집 사전 비용 최적화¶
Fluentd 로그 수집을 예로 들어, <match> </match>에서 로그 집계를 수행하여 로그를 압축하거나, <match> </match>에서 이벤트 필터링을 사용하여 오류 또는 경고 로그만 Guance에 업로드하여 사용 비용을 절감할 수 있습니다.

