본문 바로가기

인프라

Grafana Loki와 LogQL 에 대해 알아보기

최근에 Grafana Loki 사용법을 익히고, 대시보드 구성을 연구하는데 많은 시간을 들이게 되었다.
이와 관련해 Loki의 개념과 자꾸 헷갈리는 LogQL 문법을 공식 문서 기반으로 정리해보려 한다. 

Loki란? 

Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is designed to be very cost effective and easy to operate. It does not index the contents of the logs, but rather a set of labels for each log stream.

로키는 Prometheus에서 영감을 받은 로그 집계 시스템으로 다음과 같은 특징을 가지고 있다. 


➕ Prometheus란?

  • 시스템 및 서비스 모니터링 시스템으로 다음과 같은 특징이 있다.

더 자세한 내용과 아키텍처 구성은 해당 글에 자세히 정리되어 있다. 


특징

  • 다양한 클라이언트를 사용해여 모든 데이터 소스에 대해 모든 형식의 로그를 보낼 수 있다.
  • 100% 지속성을 가지고 있으며(기간 내 모든 로그를 저장함), 높은 처리량, 비용 효율적이다.
  • 메트릭 대시보드를 구축할 수 있으며, 임계치에 대해 알림을 설정할 수 있다.
  • 수집하는 로그 형식에 대한 요구사항이 없기 때문에 유연성을 제공한다. 
  • 실시간, 특정 시간 로그 조회가 가능하다. 
  • Prometheus에서 사용하는 레이블을 사용해 로그 스트림을 인덱싱, 그룹화하므로 프로메테우스와 통합하여 사용하기 편리하다.
  • K8s의 pod 로그를 저장하는데 특히 적합한데, pod 레이블과 같은 메타 데이터가 자동으로 스크랩되고 인덱싱 된다.
  • Grafana v6.0부터 기본 지원이 있다.

또한 Loki는 로그 라인의 전체 텍스트가 아닌 메타데이터만 인덱싱한다.

https://grafana.com/oss/loki/

때문에 다음과 같은 장점이 존재한다. 

  • Json 객체로 모든 내용을 인덱싱 하는 Elasticsearch에 비해 더 저렴한 운영 비용
  • 빠른 쓰기, 빠른 쿼리, 더 간단한 조작

Loki 동작 방식

먼저 Loki 기반의 로깅 스택은 다음과 같은 구성요소로 이루어져있다. 

Pomtail

Loki

  • 로그를 저장하고 쿼리를 처리하는 메인 서버
  • 로그 텍스트를 index하지 않고, stream으로 그룹화되고, label로 인덱싱 

Grafana

  • 쿼리 조회와 로그 시각화를 위한 모니터링 툴
  • 쿼리 언어로는 LogQL을 사용

➕ ELK에 대입해본다면

  • Elasticsearch = Loki : 로그 저장소, 메인 서버
  • Fluent-bit = Promtail : 로그 수집기
  • Kibana = Grafana : 모니터링 툴

(Promtail-Loki-Grafana를 줄여서 PLG라고 한단다.)

  1. Promtail 가 로그를 수집하고 Loki에 저장 전, 로그에 label 지정, 변환, 필터링 등을 처리
  2. Loki에 로그 저장
  3. LogQL을 사용해 탐색
  4. Alter : Loki Syslog 데이터를 평가하고 경고 규칙을 설정할 수 있다. 경고가 생성되면 Prometheus Alertmanager로 전송한다. 

Loki의 아키텍쳐

https://www.atatus.com/blog/a-beginners-guide-for-grafana-loki/

구성요소

Distributor

클라이언트 데이터 스트림은 Distributor가 검증하고 처리한다. 
여기서 유효한 데이터는 청크로 분할되고,병렬 처리를  위해 여러 Ingester로 전송한다.

Ingester

Distributor로 전달받은 데이터들을 Storage에 넘겨주는 역할을 한다. 
데이터는 장기 저장소에 저장되는데, 여기서 수집한 로그 데이터를 그대로 저장하는 것이 아닌 메타 데이터를 인덱싱한다. 
만약 여기서 Ingester가 충돌하면, 로그를 유실하게 된다.

Querier

수집 및 Object storage에 대한 사용자 쿼리를 처리하는데 사용된다. 
쿼리는 먼저 로컬 저장소에 대해 수행된 다음 로그를 발견하지 못하면 장기 저장소에 대해 수행된다. 
만약 타임스탬프가 동일한 데이터, 레이블 데이터, 로그 데이터가 있더라고 내부 매커니즘 때문에 데이터를 한번만 반환한다. (즉, 중복 x)

Query Frontend

쿼리에 대한 api를 제공하여 방대한 쿼리를 병렬화 할 수 있다. 
큰 검색을 작은 검색으로 나누고, 로그 읽기를 병렬로 수행한다. 

Object Stores

로키는 쿼리 가능한 로그를 추적하기 위한 long-term data storage가 필요하다. 
또한, ingester에 의해 작성된 압축된 청크, 청크 데이터의 key-value 쌍을 저장하기 위한 object storage가 필요하다. 
long-term data storage의 데이터는 로컬 저장소의 데이터보다 검색하는데 시간이 더 오래 걸릴 수 있다. 
이 object storage로 파일 시스템을 사용할 수 있지만, 확장성, 내구성, 고가용성이 없다는 단점이 있기 때문에 개발 환경 단계에서만 적정하고, 운영 환경에서는 S3, GCS와 같은 클라우드 스토리지를 사용하는 것이 적절하다. 


Grafana Loki 사용해보기 - LogQL 문법 

Grafana Loki를 직접 설치하고 환경을 구성하는 예제는 담지 않았습니다. 
(왜냐면 실제 설치 & 환경 구성을 할 일이 없기 때문이죠)
여기서는 그라파나에서 쿼리를 조회하기 위한 LogQL 작성 법을 위주로 다루겠습니다. 

먼저 LogQL 쿼리는 두 가지 유형으로 구분할 수 있다. 

  • Log queries
  • Metric queries

Log queries

https://grafana.com/docs/loki/v2.6.x/logql/log_queries/

  • 로그 라인의 내용을 반환한다. 
  • log stream selector, log pipeline으로 구성된다.

Log stream selector

  • 모든 LogQL 쿼리에는 log stream selector가 포함되어야 한다.
  • 쿼리 결과에 포함할 로그 스트림을 결정한다. 
    • 로그 스트림은 파일과 같은 로그 콘텐츠의 고유한 소스
  • log stream selector에서 라벨을 많이 지정할 수록 검색된 스트임의 수를 줄일 수 있기 때문에 쿼리 성능에 좋다. 
  • 하나 이상의 쉽표로 구분된 key-value 쌍으로 지정된다. 
  • 각 key는 로그의 label, value는 해당 label의 값이다. 
  • 중괄호 {} 는 stream selector를 구분한다. 
  • label matching operator를 사용할 수 있다.

label matching operator

  • =: 정확히 일치
  • !=: 일치하지 않음
  • =~: 정규식 일치
  • !~: 정규식 일치하지 않음 

label matching operator 예시

  • {name =~ "mysql.+"}
  • {name !~ "mysql.+"}
  • {name !~ `mysql-\d+`}

Log pipeline

  • 선택한 로그 스트림에 적용할 수 있는 표현식으로, 각 표현식은 로그 라인과 해당 레이블을 필터링, 구문 분석, 변경할 수 있다.
    • 즉 stream selector에 대한 필터링이 아닌 로그 텍스트에 대한 pipline이다.
  • 선택적으로 stream selector 뒤에 올 수 있다. 즉 필수가 아니다.  
  • 각 표현식은 각 로그 라인에 대해 왼쪽 > 오른쪽 순으로 실행된다. 
  • 표현식이 로그 라인을 필터링 하면 파이프라인은 현재 로그 라인 처리를 중지하고 다음 로그 라인 처리를 시작한다. 

Log pipeline 표현식은 아래와 같이 분류할 수 있다.

Line Filter expression

라인 필터 표현식에는 필터 연산자와 텍스트, 정규 표현식이 포함할 수 있다. 
필터 연산자는 다음과 같다. 

  • |=: 로그 라인에 문자열이 포함되어 있음
  • !=: 로그 행에 문자열이 포함되어 있지 않음
  • |~: 로그 행에 정규식과 일치하는 항목이 포함됨
  • !~: 로그 행에 정규식과 일치하는 항목이 없음

필터 연산자는 연결될 수 있으며, 순차적으로 적용된다. 
정규식으로는 RE2 구문 을 사용할 수 있다. 

Label filter expression

기존 label과 커스텀하게 추출한 label을 사용해 로그 라인을 필터링 할 수 있다. 
label 식별자, operation, value를 포함할 수 있다.

  • String : "" 또는 ``와 같이 큰따옴표, 역 따옴표로 표시한다. 
    • log stream selector에서 사용하는 것과 동일한 operation을 사용할 수 있다. (=,!=,=~,!~)
  • Durataion : 유효한 단위는 "ns", "us", "ms", "s", "m", "h"
  • Number : 숫자, 부동 소수점도 허용
  • Byte : 유효한 단위는 "b", "kib", "kb", "mib", "mb", "gib", "gb", "tib", "tb", "pib", "pb", "eib"

Durataion, Number, Byte는 and, or로 표현식을 연결할 수 있으며, 다음 comparators를 사용할 수 있다.

  • == or = : 동등
  • != : 동등 x
  • > and >= 
  • < and <=

ex) 

| duration >= 20ms or method="GET" and size <= 20KB
| ((duration >= 20ms or method="GET") and size <= 20KB)

Parser expression

로그 콘텐츠에서 label을 추출할 수 있다. 여기서 추출한 label은 Label filter expression을 사용해 필터링, 메트릭 집계에서 사용할 수 있다. 

parser의 종류는 다음과 같다. 

json

  • 모든 json의 속성이 레이블로 추출된다. 
  • 참고로 배열은 건너뛴다.
  • | json 

ex) 

{
    "protocol": "HTTP/2.0",
    "servers": ["129.0.1.1","10.2.1.3"],
    "request": {
        "time": "6.032",
        "method": "GET",
        "host": "foo.grafana.net",
        "size": "55",
        "headers": {
          "Accept": "*/*",
          "User-Agent": "curl/7.68.0"
        }
    },
    "response": {
        "status": 401,
        "size": "228",
        "latency_seconds": "6.031"
    }
}
"protocol" => "HTTP/2.0"
"request_time" => "6.032"
"request_method" => "GET"
"request_host" => "foo.grafana.net"
"request_size" => "55"
"response_status" => "401"
"response_size" => "228"
"response_latency_seconds" => "6.031"

logfmt

  • logfmt 의 형식을 사용한다. 
  • | loffmt

ex)

at=info method=GET path=/ host=grafana.net fwd="124.133.124.161" service=8ms status=200
"at" => "info"
"method" => "GET"
"path" => "/"
"host" => "grafana.net"
"fwd" => "124.133.124.161"
"service" => "8ms"
"status" => "200"

pattern

  • | pattern "<pattern-expression>"
  • 패턴 표현식을 정의해 로그 라인에서 필드를 명시적으로 추출할 수 있다. 
0.191.12.2 - - [10/Jun/2021:09:14:29 +0000] "GET /api/plugins/versioncheck HTTP/1.1" 200 2 "-" "Go-http-client/2.0" "13.76.247.102, 34.120.177.193" "TLSv1.2" "US" ""
# 표현식
<ip> - - <_> "<method> <uri> <_>" <status> <size> <_> "<agent>" <_>


# 추출 결과
"ip" => "0.191.12.2"
"method" => "GET"
"uri" => "/api/plugins/versioncheck"
"status" => "200"
"size" => "2"
"agent" => "Go-http-client/2.0"

Regular expression

unpack

  • | unpack
  • json 로그 라인을 구문 분석하고, Promtail의 단계에서 포함된 모든 레이블의 압축을 푼다. 

Line format expression

  • | line_format "{{.label_name}}"
  •  text/template 형식을 사용해 로그 라인 내용을 다시 쓸 수 있다. 

ex)

{container="frontend"} | logfmt | line_format "{{.query}} {{.duration}}"

Labels format expression

  • | label_format
  • label의 이름을 바꾸거나 수정, 추가할 수 있다. 

Metric queries

LogQL은 Range Vector(범위 벡터) 개념을 사용한다. 
(즉, 각 시계열에 대한 시간 경과에 따른 데이터 포인트 범위를 포함하는 시계열 세트)
집계는 일정 기간동안 적용되며, Time Duration 정의가 필요하다. 

Loki는 두가지 유형의 범위 벡터 집계를 제공한다. 

  • log range aggregations
  • unwrapped range aggregations

Log range aggregations

Duration 동안 쿼리를 집계하는 함수를 적용한다. 
Duration은 log stream selector 뒤, log pipline 끝에 배치해야 한다. 

  • rate(log-range): 초당 항목 수
  • count_over_time(log-range): 주어진 범위 내에서 각 로그 스트림에 대한 항목
  • bytes_rate(log-range): 각 스트림의 초당 바이트 수
  • bytes_over_time(log-range): 주어진 범위에 대해 각 로그 스트림이 사용하는 바이트의 양
  • absent_over_time(log-range): 전달된 범위 벡터에 요소가 있는 경우 빈 벡터, 전달된 범위 벡터에 요소가 없는 경우 값이 1인 요소를 1개 가진 벡터 반환 
    • 특정 시간 동안 레이블 조합에 대한 시계열 및 로그 스트림이 없을 때 경고하는 데 유용

ex)

  • MySQL 작업에 대해 지난 5분 이내에 모든 로그 라인 계산
count_over_time({job="mysql"}[5m])

 

Unwrapped range aggregations

추출한 label을 로그 라인 대신 샘플 값으로 사용한다. 
자세한 내용은 공식 문서 참고

참고 자료

더 읽어보면 좋을 글 

'인프라' 카테고리의 다른 글

Saga 패턴  (0) 2022.09.18
Nginx를 이용한 로드 밸런싱, 무중단 배포까지의 고찰  (0) 2021.10.28
AWS CloudFront와 S3 구성  (0) 2021.08.23
생활코딩 OAuth 2.0 강의와 놀토의 OAuth  (0) 2021.08.20
Flyway 적용기  (0) 2021.08.15