This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
proposed @vespian_gl @mappelman @sguyon @nicholasklick monitor observability 2023-10-29

GitLab Observability - Logging

요약

이 설계 문서는 추적메트릭과 함께 GitLab Observability Backend (GOB)의 일부가 될 로그를 저장하고 쿼리하는 시스템에 대해 개요합니다. 기본적으로 시스템은 데이터 입력을 위해 OpenTelemetry logging 명세를 활용하고 저장을 위해 ClickHouse 데이터베이스를 사용합니다. 사용자는 GitLab UI를 통해 데이터와 상호 작용할 것입니다. 시스템 자체는 다중 테넌트이며 사용자는 자신의 응용 프로그램 로그를 저장하고 쿼리하며 미래의 반복작업에서 다른 관측 가능한 신호 (추적, 오류, 메트릭 등)와 연관시킬 수 있습니다.

동기

추적메트릭 이후 로깅은 사용자에게 완전히 통합된 관측 가능한 솔루션을 제공하기 위해 지원해야 하는 마지막 관측 가능한 신호입니다.

로그 자체가 매우 퍼져 있기 때문에 로깅 자체도 중요한 관측 가능한 신호로 볼 수 있습니다. 애플리케이션 관측 가능성의 역사에서 메트릭 및 추적보다 먼저 구현되는 경우가 많습니다.

로그 지원이 없으면 사용자가 저희 플랫폼을 사용하여 개발한 응용 프로그램의 성능 및 운영을 완전히 이해하는 것이 매우 어려울 것입니다.

목표

  • 다중 테넌트: 각 사용자와 그들의 데이터는 플랫폼을 사용하는 다른 사람들과 격리되어야 합니다. 사용자는 플랫폼으로 보낸 자신의 데이터만 쿼리할 수 있습니다.
  • OpenTelemetry 표준 준수: 로그 입력은 OpenTelemetry 프로토콜을 따라야 합니다. OpenTelemetry 프로토콜에 이미 개발된 도구 및 노하우를 재사용할 뿐만 아니라 와이어 프로토콜과 데이터 저장 형식에 대해 새롭게 개발할 필요가 없습니다.
  • ClickHouse를 데이터 저장 백엔드로 사용: ClickHouse는 GitLab에서 관측 가능한 데이터의 표준 솔루션이 되었습니다. 우리의 추적 및 메트릭 솔루션이 이미 사용 중이므로 Logging도 일관성 있게 되고 새로운 의존성을 도입하지 않을 것입니다.
  • 사용자는 상당히 복잡한 쿼리를 사용하여 데이터를 쿼리할 수 있어야 함: 로그를 저장하는 것만으로는 사용자에게 큰 가치를 제공하지 못할 것입니다.

비목표

  • 복잡한 쿼리 지원 및 로그 분석 - 적어도 첫 번째 반복에서는 사용자가 수량적인 로그 분석에 사용할 수 있는 GROUP BY 쿼리를 지원하는 계획은 없습니다. 이를 지원하는 것은 간단하지 않으며 쿼리 언어 구문에 대한 추가 연구 및 작업이 필요합니다.
  • 고급 데이터 유지 - 로그는 법적 요구 사항에 관해 추적 및 메트릭과 다릅니다. 당국은 예를 들어 진행 중인 조사의 일환으로 저장된 로그를 요청할 수 있습니다. 초기 반복에서는 우리 시스템이 이에 대비하기에 준비되지 않았기 때문에 사용자들에게 주의를 경계해야 할 것이며, 현재는 액세스 로그를 저장할 경우 보조 시스템이 필요합니다. 이 사용 사례를 처리하기 위해 로그/데이터 무결성 및 장기 저장 정책에 대해 추가로 더 많은 작업이 필요합니다.
  • 데이터 삭제 - 데이터가 정의된 저장 기간 후에 만료되는 경우를 제외하고 개별 로그를 사용자가 삭제할 수 있도록 지원할 계획은 없습니다. 이는 나중에 남겨둘 것입니다.
  • 추적과 로그를 연결하는 것 - 첫 번째 반복에서는 추적을 로그에 연결하는 것을 의도하지 않습니다. 적어도 UI에서는 지원하지 않을 것입니다.
  • 로그 샘플링 - 추적에서는 사용자가 데이터를 보내기 전에 샘플링을 예상하는데, 우리는 한정된/할당량만 강제하도록 중점을 둘 것입니다. 로그도 이 패턴을 따라야 합니다. 로그 샘플링 구현은 불완전한 것처럼 보입니다 - 로그 샘플러는 OTEL Collector에 구현되어 있음에도 불구하고, 추적 샘플링과 함께 작동할 수 있는지 명확하지 않으며, 공식 명세가 없습니다 (이슈, 풀 리퀘스트).

제안

로그 입력 아키텍처는 추적메트릭 제안서에서 개요된 패턴을 따릅니다:

시스템 개요

이러한 제안서에서 소개된 컴포넌트를 재사용하기 때문에 새로운 서비스가 추가되지는 않을 것입니다. 각 최상위 GitLab 네임스페이스에는 클러스터 전체 Ingress가 사용합니다. 또한, 사용자 요청의 속도 제한이 Ingress 수준에서 수행됩니다. 클러스터 전체 Ingress는 현재 Traefik을 사용하며, 모든 다른 서비스와 공유됩니다.

입력 경로

우리는 JSON 형식의 Log 개체를 고객으로부터 HTTP를 통해 받습니다. 이 요청은 클러스터 전체 Ingress로 도착하며, 이를 OTEL 컬렉터에 의해 처리되며 ClickHouse에 대해 INSERT 문을 실행합니다.

읽기 경로

GOB은 예를 들어 GitLab UI가 로그를 쿼리하고 나중에 렌더링하는 데 사용하는 HTTP/JSON API를 노출합니다. 클러스터 전체 Ingress는 요청을 쿼리 서비스로 라우팅하고 쿼리 서비스는 API 요청을 구문 분석하고 ClickHouse에 대해 SQL 쿼리를 실행합니다. 결과는 JSON 응답으로 서식을 지정하고 클라이언트에게 다시 보냅니다.

설계 및 구현 세부 정보

레거시 코드

로그 신호 처리는 지지해야 하는 상당한 양의 레거시 코드에 심각한 영향을 받습니다. 메트릭 및 추적과는 다르게 OpenTelemetry 명세는 새로운 API 및 SDK를 활용할 수 있습니다. 로그의 경우 OpenTelemetry는 더 많은 다리 역할을 하며 레거시 라이브러리/코드가 우리에게 데이터를 전송할 수 있도록 만듭니다.

기존 로그 라이브러리는 파일 로그 수신기fluentd를 사용하여 일반 로그 파일에서 Log 신호를 생성할 수 있습니다. 기존 로그 라이브러리는 로그 브릿지 API를 사용하여 OTEL 프로토콜을 사용하여 로그를 전송할 수 있습니다. 시간이 흘러 이 생태계는 아마도 발전할 것이며 옵션의 수는 늘어날 것입니다. 가정은 사용자가 로그를 입력하는 방법은 사용자에게 따라 있다는 것입니다.

그러므로 우리는 이미 적절하게 구문 분석되고 서식이 지정된 것으로 가정하고 OTEL 형식의 로그를 수락하는 HTTP 엔드포인트만 노출합니다.

로그, 이벤트 및 Span 이벤트

OTEL 명세에 따르면 로그 메시지는 세 가지 서로 다른 객체를 사용하여 전송할 수 있습니다:

적어도 첫 번째 반복에서는 Logs, Events 또는 Span-Events 중 하나만 지원할 수 있습니다. Span 이벤트를 보낼 수는 없으며 이러한 이벤트를 구현하는 대다수의 레거시 코드 때문에 불가능하거나 원하지 않을 것입니다.

데이터 모델 내에서 Events가 Logs와 동일하게 사용된다고 하더라도 의미론적으로 다릅니다. 로그에는 필수적으로 심각도 수준이 있으므로 첫 번째 클래스 매개변수로 필요합니다. 이벤트는 이러한 필수 요소가 없어도 되지만 Log 레코드의 Attributes 필드에는 event.name과 선택적으로 event.domain 키가 필요합니다. 또한 로그는 일반적으로 문자열 형식의 메시지을 갖고 있지만, 이벤트는 키-값 쌍의 데이터를 갖고 있습니다. 두 가지 사이의 서로 다른점에 대한 자세한 내용은 여기에서 찾을 수 있습니다.

개발자/잠재적 사용자의 관점에서 보면 로그 레코드로 모델링할 수 없는 로깅 사용 사례가 없을 것으로 보입니다. 예를 들어 커뮤니티가 여기 (https://github.com/open-telemetry/opentelemetry-specification/issues/3254)나 여기 (https://github.com/open-telemetry/oteps/blob/main/text/0202-events-and-logs-api.md#subtle-differences-between-logs-and-events)에 제시한 예는 설득력이 없으며 단순히 로그 레코드로 모델링할 수 있을 것입니다.

따라서 Log 개체만 지원하기로 한 결정은 지루하고 간단한 해법으로 보입니다.

속도 제한

추적과 유사하게 로깅 데이터 입력은 Ingress 수준에서 수행됩니다. 포워드-인증 플로우의 일환으로 Traefik은 요청을 Gatekeeper로 전달하고, Gatekeeper는 카운팅을 위해 Redis를 활용합니다. 현재 이 작업은 입력 경로에 대해서만 수행됩니다. 작동 방식에 대한 자세한 내용은 MR 설명에서 확인하세요. 읽기 경로 속도 제한 구현은 여기에서 추적되고 있습니다.

데이터베이스 스키마

OpenTelemetry 명세는 구현에서 필요한 일련의 필드를 정의합니다. 문서화된 스키마와 protobuf 정의 사이에는 일부 작은 불일치가 있습니다. TraceFlags는 문서에서 8비트 필드로 정의되어 있지만, proto 정의에서는 32비트 폭의 필드로 정의되어 있습니다. 나머지 24비트는 예약되어 있습니다. 로그 메시지 본문은 임의의 문자열이나 예를 들어 JSON일 수 있으며 기록에 대한 크기 제한이 없습니다. 이 설계 문서의 목적을 위해 우리는 일부분의 임의의 문자열로 가정할 것입니다. 일반 텍스트이든 JSON이든 길이 제한이 없도록 할 것입니다.

데이터 필터링

스키마는 Bloom 필터를 광범위하게 사용합니다. 이들은 거짓 부정을 방지하지만 거짓 긍정은 여전히 가능하므로 사용자에게 != 쿼리를 제공할 수 없을 것입니다. Body 필드는 tokenbf_v1을 사용하는 토큰화된 Bloom 필터입니다. tokenbf_v1 스킵 인덱스는 ngrambf_v1 인덱스보다 더 간단하고 가벼운 접근 방식으로 보입니다. 아래의 매우 초기의 벤치마크를 기반으로 ngrambf_v1 인덱스를 조정하는 것은 훨씬 더 어려울 것으로 예상됩니다. 단점은 현재 사용자가 단어의 전체에 대해서만 검색할 수 있다는 것입니다. 특정 구간의 단어 수가 최대 10,000개까지 될 것으로 추정되며, 거짓 긍정 가능성은 0.1%로 목표로 합니다. 이 도구를 사용하여 필터의 최적 크기는 143776비트 및 10개의 해시 함수로 계산되었습니다.

인덱스 스킵, ==, !=LIKE 연산자

스킵 인덱스는 검색할 구간을 최적화합니다. ==LIKE 쿼리는 원하는대로 작동하지만, !=는 항상 Bloom 필터의 제한으로 전체 스캔 결과가 됩니다. 최소한 첫 번째 반복에서는 사용자에게 != 연산자를 사용할 수 없도록 할 것입니다.

데이터에 기반하여, 첫 번째 반복에서는 tokenbf_v1 필터를 조정하는 것이 ngrambf_v1보다 훨씬 쉬울 수 있습니다. 그 이유는 ngrambf_v1 쿼리의 경우 n-gram의 수가 토큰보다 훨씬 많기 때문에 고기능 문자/기호 데이터에 대해 일치가 더 빈번하기 때문입니다.

매우 초기의 벤치마크를 확인하기 위해 다음과 같은 테이블 스키마 및 삽입/함수를 사용하였습니다. 우리는 Body 필드에만 집중하고 싶기 때문에 단일 테넌트를 시뮬레이션하고자 했습니다.

tokenbf_v1 버전의 테이블:

CREATE TABLE tbl2
(
    `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
    `TraceId` String CODEC(ZSTD(1)),
    `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
    `Duration` UInt8 CODEC(ZSTD(1)),
    `SpanName` LowCardinality(String) CODEC(ZSTD(1)),
    `Body` String CODEC(ZSTD(1)),
    INDEX idx_body Body TYPE tokenbf_v1(143776, 10, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
SETTINGS index_granularity = 8192

ngrambf_v1 버전의 테이블:

CREATE TABLE tbl3
(
    `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
    `TraceId` String CODEC(ZSTD(1)),
    `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
    `Duration` UInt8 CODEC(ZSTD(1)),
    `SpanName` LowCardinality(String) CODEC(ZSTD(1)),
    `Body` String CODEC(ZSTD(1)),
    INDEX idx_body Body TYPE ngrambf_v1(4,143776, 10, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
SETTINGS index_granularity = 8192

두 경우 모두 Body 필드가 JSON 맵을 시뮬레이션하는 데이터로 채워져 있었습니다:

CREATE FUNCTION genmap AS (n) -> arrayMap (x-> (x::String, (x*(rand()%40000+1))::String), range(1, n));

INSERT INTO tbl(2|3)
SELECT
    now() - randUniform(1, 1_000_000) as Timestamp,
    randomPrintableASCII(2) as TraceId,
    randomPrintableASCII(2) as ServiceName,
    rand32() as Duration,
    randomPrintableASCII(2) as SpanName,
    toJSONString(genmap(rand()%40+1)::Map(String, String)) as Body
FROM numbers(10_000_000);

tokenbf_v1 테이블의 경우:

  • == 동일성은 작동하며, 스킵 인덱스를 통해 224/1264 개의 구간이 스캔되었습니다:
zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl2 where Body == '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

EXPLAIN indexes = 1
SELECT count(*)
FROM tbl2
WHERE Body = '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

Query id: 60827945-a9b0-42f9-86a8-dfe77758a6b1

┌─explain───────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))       │
│   Aggregating                                     │
│     Expression (Before GROUP BY)                  │
│       Filter (WHERE)                              │
│         ReadFromMergeTree (logging.tbl2)          │
│         Indexes:                                  │
│           MinMax                                  │
│             Condition: true                       │
│             Parts: 69/69                          │
│             Granules: 1264/1264                   │
│           Partition                               │
│             Condition: true                       │
│             Parts: 69/69                          │
│             Granules: 1264/1264                   │
│           PrimaryKey                              │
│             Condition: true                       │
│             Parts: 69/69                          │
│             Granules: 1264/1264                   │
│           Skip                                    │
│             Name: idx_body                        │
│             Description: tokenbf_v1 GRANULARITY 1 │
│             Parts: 62/69                          │
│             Granules: 224/1264                    │
└───────────────────────────────────────────────────┘

23 rows in set. Elapsed: 0.019 sec.
  • != 부등성 또한 작동하지만 전체 텍스트 스캔 결과가 되었습니다 - 모든 구간이 스캔되었습니다:
zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl2 where Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

EXPLAIN indexes = 1
SELECT count(*)
FROM tbl2
WHERE Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

Query id: 01584696-30d8-4711-8469-44d4f2629c98

┌─explain───────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))       │
│   Aggregating                                     │
│     Expression (Before GROUP BY)                  │
│       Filter (WHERE)                              │
│         ReadFromMergeTree (logging.tbl2)          │
│         Indexes:                                  │
│           MinMax                                  │
│             Condition: true                       │
│             Parts: 69/69                          │
│             Granules: 1264/1264                   │
│           Partition                               │
│             Condition: true                       │
│             Parts: 69/69                          │
│             Granules: 1264/1264                   │
│           PrimaryKey                              │
│             Condition: true                       │
│             Parts: 69/69                          │
│             Granules: 1264/1264                   │
│           Skip                                    │
│             Name: idx_body                        │
│             Description: tokenbf_v1 GRANULARITY 1 │
│             Parts: 69/69                          │
│             Granules: 1264/1264                   │
└───────────────────────────────────────────────────┘

23 rows in set. Elapsed: 0.017 sec.
  • LIKE 쿼리는 작동하며, 271/1264 구간이 스캔되었습니다:
zara.engel.vespian.net :) explain indexes=1 select * from tbl2 where Body like '%"11":"162052"%';

EXPLAIN indexes = 1
SELECT *
FROM tbl2
WHERE Body LIKE '%"11":"162052"%'

Query id: 86e99d7a-6567-4000-badc-d0b8b2dc8936

┌─explain─────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY)) │
│   ReadFromMergeTree (logging.tbl2)          │
│   Indexes:                                  │
│     MinMax                                  │
│       Condition: true                       │
│       Parts: 69/69                          │
│       Granules: 1264/1264                   │
│     Partition                               │
│       Condition: true                       │
│       Parts: 69/69                          │
│       Granules: 1264/1264                   │
│     PrimaryKey                              │
│       Condition: true                       │
│       Parts: 69/69                          │
│       Granules: 1264/1264                   │
│     Skip                                    │
│       Name: idx_body                        │
│       Description: tokenbf_v1 GRANULARITY 1 │
│       Parts: 64/69                          │
│       Granules: 271/1264                    │
└─────────────────────────────────────────────┘

20 rows in set. Elapsed: 0.047 sec.

ngrambf_v1 토크나이저를 올바르게 사용하고 튜닝하는 것은 훨씬 더 어려울 것입니다.

  • n-gram 인덱스를 사용한 동일성 또한 작동하지만, Bloom 필터의 높은 정밀도로 인해 많은 구간을 건너뛰지 못하였습니다:
zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl3 where Body == '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

EXPLAIN indexes = 1
SELECT count(*)
FROM tbl3
WHERE Body = '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

Query id: 22836e2d-5e49-4f51-b23c-facf5a3102c2

┌─explain───────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))       │
│   Aggregating                                     │
│     Expression (Before GROUP BY)                  │
│       Filter (WHERE)                              │
│         ReadFromMergeTree (logging.tbl3)          │
│         Indexes:                                  │
│           MinMax                                  │
│             Condition: true                       │
│             Parts: 60/60                          │
│             Granules: 1257/1257                   │
│           Partition                               │
│             Condition: true                       │
│             Parts: 60/60                          │
│             Granules: 1257/1257                   │
│           PrimaryKey                              │
│             Condition: true                       │
│             Parts: 60/60                          │
│             Granules: 1257/1257                   │
│           Skip                                    │
│             Name: idx_body                        │
│             Description: ngrambf_v1 GRANULARITY 1 │
│             Parts: 60/60                          │
│             Granules: 1239/1257                   │
└───────────────────────────────────────────────────┘

23 rows in set. Elapsed: 0.025 sec.
  • 여기에서의 부등성 역시 전체 스캔 결과가 되었습니다:
zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl3 where Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

EXPLAIN indexes = 1
SELECT count(*)
FROM tbl3
WHERE Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'

Query id: 2378c885-65b0-4be0-9564-fa7ba7c79172

┌─explain───────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))       │
│   Aggregating                                     │
│     Expression (Before GROUP BY)                  │
│       Filter (WHERE)                              │
│         ReadFromMergeTree (logging.tbl3)          │
│         Indexes:                                  │
│           MinMax                                  │
│             Condition: true                       │
│             Parts: 60/60                          │
│             Granules: 1257/1257                  

#### 데이터 중복 제거

비용 효율적인 서비스를 제공하기 위해 사용자로부터 받은 데이터 중복 처리에 대해 고려해야 합니다.
ClickHouse [ReplacingMergeTree](https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replacingmergetree)는 기본 키를 기반으로 데이터를 자동으로 중복 처리합니다.
우리는 기본 필드에 모든 관련 `Log` 항목을 포함할 수 없습니다. 따라서 기본 키의 매우 마지막 부분에 Fingerprint를 넣는 것이 아이디어입니다.
이를 통해 고유 레코드가 삭제되지 않도록 색인을 작성하는 데 사용하지만 색인용으로는 사용하지 않습니다.
Fingerprint 계산 알고리즘과 길이는 아직 선택되지 않았으며, metrics에서 Fingerprint를 계산하는 데 사용 중인 것과 동일한 것을 사용할 수 있습니다.
지금으로서는 128비트 너비(16개의 8비트 문자)로 가정합니다.
Fingerprint 계산에 사용하는 열은 기본 키에 없는 열입니다: `Body`, `ResourceAttributes`, `LogAttributes`.
매우 높은 카디널리티 때문에 Fingerprint는 기본 인덱스의 마지막 자리로 들어가야 합니다.

#### 데이터 보관 기간

로그의 보관 기간과 그들의 삭제를 허용할지 여부에 대한 법적 문제가 있습니다(예: 일부 개인 데이터 또는 조사와 관련된 데이터 유출로 인한 삭제).
특정 지역 법률에 따르면, 로그를 몇 년 동안 보관해야 하며 삭제할 수 있는 방법이 있어서는 안 됩니다.
이는 ObservedTimestamp를 Fingerprint에 포함해야 하는 것에 영향을 미칩니다.
`Non-Goals` 섹션에서 지적된 것처럼, 이 문제는 향후 반복에서 다루려고 합니다.

#### 삽입 시간 필드

사용자가 무수히 많은 로그 형식을 사용할 것이기 때문에 [의미론적 관례 필드](https://opentelemetry.io/docs/specs/semconv/general/logs/)를 별도의 열로 뽑아내지 않고 합치는 것이 의도된 것입니다. 그리고 이것이 열이 될 가치가 있는 속성을 식별하는 것이 어렵습니다.

`ObservedTimestamp` 필드는 수집 중에 컬렉터에 의해 설정됩니다.
사용자는 `Timestamp` 필드로 쿼리하고 로그 가지치기는 `ObservedTimestamp` 필드에 의해 이뤄집니다.
이 접근 방식의 단점은 `TTL DELETE`가 데이터를 원하는 것보다 이른 시점에 제거하지 못할 수 있으며, 기본 인덱스와 TTL 열이 다르기 때문에 데이터가 로컬화되지 않을 수 있습니다.
그러나 이것은 좋은 대가로 보입니다.
사용자에게는 삽입에서 시작하는 미리 정의된 저장 기간을 제공할 것입니다.
사용자가 미래나 과거의 타임스탬프를 가진 로그를 삽입하는 경우, 오래된 로그의 제거가 너무 이른지 또는 너무 늦은지일 수 있습니다.
사용자는 주장된 로그 타임스탬프를 지연시키기 위해서도 낡아진 로그를 낡은 것으로 현혹시킬 수 있습니다.
`ObservedTimestamp` 방식에는 이러한 문제가 없습니다.

삽입 중에 `SeverityText` 필드가 설정되지 않은 경우 `SeverityText` 필드를 `SeverityNumber`로 구문 분석합니다.
쿼리는 일반 텍스트보다 효율적이며 더 세부적인 `SeverityNumber` 필드를 사용할 것입니다.

```plaintext
로그 테이블을 삭제하려면,
logs 테이블이 존재하면 삭제합니다.
CREATE TABLE logs
(
    `ProjectId` String CODEC(ZSTD(1)),
    `Fingerprint` FixedString(16) CODEC(ZSTD(1)),
    `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
    `ObservedTimestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
    `TraceId` FixedString(16) CODEC(ZSTD(1)),
    `SpanId` FixedString(8) CODEC(ZSTD(1)),
    `TraceFlags` UInt32 CODEC(ZSTD(1)),
    `SeverityText` LowCardinality(String) CODEC(ZSTD(1)),
    `SeverityNumber` UInt8 CODEC(ZSTD(1)),
    `ServiceName` String CODEC(ZSTD(1)),
    `Body` String CODEC(ZSTD(1)),
    `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
    INDEX idx_span_id SpanId TYPE bloom_filter(0.001) GRANULARITY 1,
    INDEX idx_trace_flags TraceFlags TYPE set(2) GRANULARITY 1,
    INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_body Body TYPE tokenbf_v1(143776, 10, 0) GRANULARITY 1
)
ENGINE = ReplacingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ProjectId, ServiceName, SeverityNumber, toUnixTimestamp(Timestamp), TraceId, Fingerprint)
TTL toDateTime(ObservedTimestamp) + toIntervalDay(30)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;

쿼리 API, 쿼리 UI

이 제안에 의해 도입된 쿼리 API/워크플로의 주요 아이디어는 사용자에게 쿼리 복잡성 및 쿼리 리소스 사용/실행 시간 제한을 모두 제공하는 동시에 쿼리의 자유를 부여하는 것입니다. 사용자가 데이터를 어떻게 쿼리할지, 데이터가 정확히 어떻게 보일지를 예상할 수 없습니다. 일부 사용자는 속성을 사용하고 일부 사용자는 로그 수준에만 관심을 가질 것입니다.

Clickhouse에서는 개별 쿼리가 설정을 가질 수 있으며, 이는 쿼리 복잡성 설정을 포함합니다. 쿼리 리미트는 쿼리 서비스가 SQL 문을 생성할 때 쿼리 자동으로 추가됩니다.

Body 필드의 전체 텍스트 쿼리는 ClickHouse가 LIKE 쿼리를 BloomFilters를 사용하여 최적화하므로 쿼리 서비스가 이를 투명하게 처리할 수 있습니다. 향후 버전에서는 n-그램 토큰화를 고려할 수 있으며, 지금으로서는 쿼리가 전체 단어에만 제한될 것입니다.

사용자가 중복된 데이터를 입력하는 경우 UI에서 로그 항목의 중복을 제거할 지에 대한 논쟁이 있을 수 있습니다. 우리는 max(ObservedTimestamp) 함수를 사용하여 레코드 삽입과 ReplacingMergeTree의 최종 중복 처리 사이의 시간에 중복된 항목을 피할 수 있습니다. 그러나 첫 번째 반복에서는 명확하지 않습니다.

쿼리 서비스는 쿼리를 작성하는 동안 쿼리의 SeverityText 속성을 쿼리의 SeverityNumber로 투명하게 변환할 것입니다.

쿼리 서비스 API 스키마

시스템 남용 가능성을 열어 두지 않기 위해 UI가 SQL 쿼리를 보내지 못하도록 허용할 수 없습니다. 또한 전체적으로 SQL 쿼리 언어의 유연성을 고려할 때 사용자가 만들어낼 수 있는 모든 사용 사례를 지원할 수 없습니다. 따라서 아이디어는 UI가 사용자를 안내하면서 간단한 생성자와 유사한 경험을 제공하는 것입니다. 지금까지 GitLab이 MRs와 Issues를 검색하는 데 사용하는 것과 매우 유사합니다. 그 다음 UI 코드는 사용자가 만든 쿼리를 JSON으로 번역하고 쿼리 서비스에 처리를 보내게됩니다. 받은 JSON을 기반으로 쿼리 서비스는 쿼리 리미트와 함께 SQL 쿼리를 템플릿화합니다.

현재 UI 및 JSON API는 특정 필드에 대해 기본 설정의 일부 작업 만을 지원할 것입니다:

  • Timestamp: >, <, ==
  • TraceId: ==, 나중에 반복에서 in
  • SpanId: ==, 나중에 반복에서 in
  • TraceFlags: ==, !=, 나중에 반복에서:in, notIn
  • SeverityText: ==, !=, 나중에 반복에서: in, notIn
  • SeverityNumber: <,>, ==, !=, 나중에 반복에서: in, notIn
  • ServiceName: ==, !=, 나중에 반복에서: in, notIn
  • Body: ==, CONTAINS
  • ResourceAttributes: key==value, mapContains(key)
  • LogAttributes: key==value, mapContains(key)

중간 JSON 형식은 다음과 같을 수 있습니다:

{
  "query": [
    { "type": "()|AND|OR",
      "operands": {
        [...]
    },
    {
      "type": "==|!=|<|>|CONTAINS",
      "column": "...",
      "val": "..."
    }
  ]
}

==|!=|<|>|CONTAINS는 중첩되지 않는 오퍼랜드로, 구체적인 열에 대해 작동하며, 쿼리 서비스에서 처리된 후 WHEN 조건으로 결과를 생성합니다. ()|AND|OR는 중첩되는 오퍼랜드이며, 다른 중첩되지 않는 오퍼랜드만 포함할 수 있습니다. 우리는 중첩된 오퍼랜드의 구현을 나중의 반복에서 연기할 수 있습니다. 쿼리 구조의 최상위 수준에서 오퍼랜드 간에 암시적인 AND가 있습니다.

쿼리 스키마는 메트릭 제안에서 사용된 스키마와 비교하여 의도적으로 간단하게 유지되어 있습니다. [QueryContext], BackendContext 등의 필드는 나중의 반복에서 필요에 따라 추가할 수 있습니다. 지금으로서는 스키마를 가능한 한 간단하게 유지하고 API가 쉽게 변경될 수 있도록 버전 관리를 해야 합니다.

오픈 질문

로깅 SDK 성숙도

OTEL 표준은 추적과 마찬가지로 로깅을 위한 독립적인 SDK를 제공하고 있지 않습니다. 프로그래밍 언어에 따라 매우 드물게 사용하지 않는 로깅 라이브러리를 위해 이를 고려할 수 있습니다. 기존의 모든 로깅 라이브러리는 대신 OTEL 수집기와 상호 작용하기 위해 브릿지 API를 사용하거나 OTEL 로그 표준을 사용하여 로그를 전송해아 합니다.

대부분의 언어는 필요한 수정 사항을 이미 완료했으며, Go를 제외하고는 이미 이에 대한 대부분의 지원이 이루어졌습니다. Go에 대해서는 매우 미비한 지원만이 존재합니다 (repo, repo). 공식적인 Uber Zap 리포지터리는 거의 이벤트를 스팬에 발생시키는 것에 대한 issue밖에 등록되어 있지 않습니다. Opentelemetry 상태 페이지에 따르면 Go의 지원이 아직 구현되지 않았습니다.

Go에서 네이티브 OTEL SDK 지원의 부재는 로깅을 위한 동작에 문제가 될 수 있습니다. 만약 로깅을 자체적으로 사용하고 싶다면, log 파일을 구문 분석하여 filelogreceiver 또는 fluentd를 사용하여 이 한계를 상당 부분 극복할 수 있습니다. 또한 Go의 지원을 개선하기 위한 기여와 향상 또한 유효한 옵션입니다.

향후 작업

쿼리에서 !=(같지 않음) 연산자 지원

스키마에서 사용하는 블룸 필터는 주어진 용어가 로그 항목의 내용/속성에 없는지 테스트할 수 없습니다. 이는 작지만 유효한 사용 사례입니다. 해결책으로는 역 인덱스가 될 수 있지만 이것은 아직 실험적인 기능입니다.

문서

문서 작업의 일환으로, 데이터를 GOB로 보내는 방법에 대한 예시를 오류 추적과 마찬가지로 다양한 언어에서 제공할 수 있습니다(uber-zap, logrus, log4j 등). 일부 응용 프로그램은 쉽게 수정되어 우리에게 데이터를 보낼 수 없으며(e.g. systemd/journald), filelogreceiverfluentd를 사용하여 로그를 추적/구문 분석하는 방법이 채택되어야 할 것입니다. 우리의 인프라를 이용하여 위의 두 가지 경우를 모두 다룰 수 있을 것으로 예상되며, 문서에서 우리의 코드를 링크하여 제공함으로써 우리의 솔루션을 자체적으로 사용하고 GCE 로깅 솔루션이 상당히 비싸다는 점에서 돈을 절약할 수 있으며, 사용자들이 자신들의 인프라를 어떻게 계측할 수 있는지에 대한 실제적인 예시를 제공할 수 있게 될 것입니다.

이것은 구현이 끝난 후의 후속 작업 중 하나가 될 수 있습니다.

사용자 쿼리 리소스 사용량 모니터링

장기적으로, 제한의 실행에 의해 실패한 사용자 쿼리의 수와 리소스 사용량을 모니터링할 방법이 필요할 것입니다. 이를 통해 쿼리 제한을 미세 조정하고 사용자가 지나치게 제한되지 않도록 할 수 있습니다.

반복 작업

최신 정보에 대하여 참조할 수 있도록 관측 가능 그룹 계획 에픽 및 해당 연결된 이슈를 확인하십시오.