LabHub
배우기 러닝패스 코스

OTCA — 오픈텔레메트리 인증 어소시에이트 · API 와 SDK 의 조립 — 프로바이더·프로세서·계측기·로그 모델 · 이론

계측기 일곱 종류, 로그 레코드 열두 필드, 그리고 스키마 URL

LabHub 에서 이어서 보기

한 줄 요약

메트릭 API 의 계측기(instrument)는 Counter·UpDownCounter·Gauge·Histogram 과 그 비동기(callback) 변형으로 나뉘고, SDK 가 내보내는 합(Sum)에는 delta 또는 cumulative 라는 temporality 가 붙습니다. 로그 시그널은 열두 개 필드를 가진 LogRecord 데이터 모델과, 기존 로깅 라이브러리를 그 모델로 잇는 브리지 API 로 이루어집니다. 스키마 URL 은 시맨틱 컨벤션이 바뀌어도 소비자가 데이터를 해석할 수 있게 하는 버전 표식입니다. 출처는 [메트릭 API](https://opentelemetry.io/docs/specs/otel/metrics/api/)·[메트릭 데이터 모델](https://opentelemetry.io/docs/specs/otel/metrics/data-model/)·[로그 데이터 모델](https://opentelemetry.io/docs/specs/otel/logs/data-model/)·[텔레메트리 스키마](https://opentelemetry.io/docs/specs/otel/schemas/) 스펙입니다.

왜 이게 필요했나

"요청 수" 와 "큐 길이" 와 "방 온도" 는 모두 숫자지만 성질이 다릅니다. 요청 수는 늘기만 하고, 큐 길이는 늘었다 줄고, 온도는 더해서 합칠 의미가 없습니다. 계측기 종류가 나뉘는 이유는 이 성질을 API 단계에서 밝혀 두어 SDK 가 알맞은 기본 집계를 고르게 하기 위해서입니다. 스펙 개요는 이것을 "원시 측정값을 기록하면 집계 방식은 최종 사용자가 고른다" 고 표현합니다.

로그는 반대 방향의 문제입니다. 이미 모든 언어에 로깅 라이브러리가 있고, 그 로그를 버리고 새 API 로 다시 쓰라고 할 수는 없습니다. 그래서 로그 시그널은 "기존 로그를 어떻게 공통 모델로 옮기고 트레이스와 이어 줄 것인가" 에서 출발합니다. 스키마도 같은 종류의 문제입니다 — 시맨틱 컨벤션의 속성 이름이 바뀌면 그 이름을 기대하던 대시보드와 백엔드가 깨집니다.

어떻게 동작하나

계측기 일곱 종류

계측기는 이름·종류·단위(선택)·설명(선택)으로 정의됩니다. [개념 문서](https://opentelemetry.io/docs/concepts/signals/metrics/)와 API 스펙의 설명을 합치면 이렇습니다.

| 계측기 | 동기/비동기 | 성질 | 기록 연산 |
| --- | --- | --- | --- |
| Counter | 동기 | 단조 증가(늘기만 함) | Add |
| Asynchronous Counter | 비동기 | 단조 증가, 관찰 시점의 누적값 보고 | 콜백 |
| UpDownCounter | 동기 | 증가·감소 모두(큐 길이) | Add |
| Asynchronous UpDownCounter | 비동기 | 더해서 합칠 수 있는 값(프로세스 힙 크기) | 콜백 |
| Gauge | 동기 | 더할 수 없는 값(소음 수준), 바뀔 때 기록 | Record |
| Asynchronous Gauge | 비동기 | 더할 수 없는 값(방 온도), 관찰 시점에 보고 | 콜백 |
| Histogram | 동기 | 분포가 의미 있는 값(요청 지연) | Record |

동기 계측기는 애플리케이션 로직 안에서 직접 호출되고, 그 측정값은 Context(즉 현재 스팬)와 연결될 수 있습니다. 비동기 계측기는 콜백을 등록해 두면 SDK 가 수집(collection) 시점에만 부릅니다 — 스펙의 예는 15초마다 센서 온도를 읽는 것입니다. 비동기 측정값은 Context 와 연결되지 않습니다. 스펙은 여기서의 동기·비동기가 프로그래밍의 async 패턴과 무관하다고 덧붙입니다. UpDownCounter 의 설명에는 "값이 단조 증가라면 Counter 를 쓰라" 는 지침이 있습니다.

각 계측기에는 기본 집계가 있고, View 로 바꾸거나 특정 계측기를 무시하거나 보고할 속성을 고를 수 있습니다. 개념 문서는 카디널리티 한계도 설명합니다 — 메트릭 스트림당 고유 속성 조합의 기본 상한은 2000 이며, 넘치면 측정값을 버리지 않고 otel.metric.overflow=true 속성 하나로 접습니다. 합계는 맞지만 접힌 측정값은 다른 속성을 잃으므로, 그 속성으로 거르는 질의는 적게 셉니다.

temporality — delta 와 cumulative

데이터 모델에서 Sum 은 AggregationTemporality(delta 또는 cumulative)와 단조(monotonic) 플래그, 그리고 시작·끝 시각을 가진 데이터 포인트들로 이루어집니다. delta 는 겹치지 않는 시간 창마다 그 구간의 값을 보고하고, cumulative 는 시작(보통 프로세스 시작)부터의 전체 합을 보고합니다. 단조 delta 합은 음수가 아니어야 하고, 단조 cumulative 합은 이전 값보다 작지 않아야 합니다.

스펙은 둘 사이의 절충으로 프로세스 재시작 감지, 비율(rate) 계산, 푸시/풀 방식을 들고, OTLP 가 둘 다 지원하며 필요하면 Delta-to-Cumulative 또는 Cumulative-to-Delta 변환을 할 수 있다고 적습니다. 히스토그램도 temporality 를 가지며, min·max 는 delta 에서 더 쓸모 있고 delta→cumulative 변환은 되지만 그 반대는 되지 않습니다. 개념 문서는 카디널리티와의 관계도 짚습니다 — delta 는 주기마다 상태를 지워 한 주기 안의 조합만 세지만, cumulative 는 상태를 유지하므로 한 번 상한에 닿으면 프로세스가 재시작될 때까지 계속 넘칩니다. Gauge 는 특정 시각의 표본값이라 temporality 가 없습니다.

로그 레코드의 필드

로그 데이터 모델은 "기존 로그 형식을 모호함 없이 이 모델로 옮길 수 있어야 한다" 는 요구에서 출발했고, 자주 쓰이는 필드는 이름 있는 최상위 필드로, 나머지는 Attributes 로 둡니다. 최상위 필드는 열두 개입니다.

| 필드 | 뜻 |
| --- | --- |
| Timestamp | 이벤트가 일어난 시각(원본 시계) |
| ObservedTimestamp | 수집 시스템이 관찰한 시각. SDK 에서 만든 로그는 Timestamp 와 같음 |
| TraceId / SpanId / TraceFlags | W3C Trace Context 의 식별자와 플래그. 요청 처리 중의 로그에 붙음 |
| SeverityText | 원본의 로그 레벨 문자열 |
| SeverityNumber | 정규화된 심각도 숫자 |
| Body | 본문 |
| Resource | 로그를 낸 개체 |
| InstrumentationScope | 로그를 낸 범위(라이브러리·모듈) |
| Attributes | 추가 정보 |
| EventName | 이벤트의 종류를 나타내는 이름 |

시각이 하나만 지원되는 곳으로 옮길 때는 Timestamp 가 있으면 그것을, 없으면 ObservedTimestamp 를 씁니다. SeverityNumber 는 1~24 범위로, 1–4 TRACE, 5–8 DEBUG, 9–12 INFO, 13–16 WARN, 17–20 ERROR, 21–24 FATAL 이며 0 은 미지정입니다. 스펙의 예처럼 17 은 20 보다 덜 심각한 오류입니다. Events 는 이 LogRecord 의 표준화된 형식이며, 로그 시맨틱 컨벤션은 Event 형식으로 정의됩니다.

브리지 API

[로그 API](https://opentelemetry.io/docs/specs/otel/logs/api/)는 첫 문장에서 대상을 밝힙니다 — "로깅 라이브러리 작성자가 로그 어펜더(appender)를 만들도록 제공되며, 어펜더는 이 API 로 기존 로깅 라이브러리와 OpenTelemetry 로그 데이터 모델 사이를 잇는다." 구조는 LoggerProvider 에서 Logger 를 얻고(name, 선택적으로 version·schema_url·attributes), Logger 가 LogRecord 를 Emit 하는 것이 전부입니다. Emit 은 Timestamp·ObservedTimestamp·Context·SeverityNumber·SeverityText·Body·Attributes·EventName 을 받고, Context 를 주지 않으면 현재 Context 를 씁니다 — 이것이 로그에 TraceId·SpanId 가 자동으로 붙는 원리입니다. Enabled 는 LogRecord 를 만드는 비용이 클 때만 먼저 물어보는 최적화이며, 호출이 필수는 아닙니다.

[로그 시그널 개요](https://opentelemetry.io/docs/specs/otel/logs/)는 수집 방식을 둘로 나눕니다. 파일이나 표준 출력으로 나온 로그를 Collector 가 읽어 파싱하는 방식(애플리케이션 변경이 거의 없지만 파싱이 어렵다)과, 어펜더를 붙여 OTLP 로 직접 보내는 방식(구조화가 잘 되고 파일·회전·파싱이 사라지지만 OTLP 를 받는 목적지가 필요하다)입니다. 어느 쪽이든 애플리케이션 개발자는 시작 시점에 어펜더와 SDK 를 설정하기만 하면 됩니다.

스키마 URL

시맨틱 컨벤션은 진화하고, 텔레메트리 소스와 소비자는 서로 다른 속도로 바뀝니다. 스키마 스펙은 세 당사자를 분리하기 위해 다음을 정합니다. 스키마는 버전이 있고(MAJOR.MINOR.PATCH), 버전 사이의 변환(예: 속성 이름 바꾸기)을 스키마 파일이 명시하며, 각 버전은 고유한 Schema URL 로 식별됩니다. URL 의 마지막 경로가 버전이고 그 앞이 스키마 패밀리입니다. 스키마 파일은 발행되면 불변이라 영구 캐시해도 됩니다. 소스(계측 라이브러리)는 내보내는 텔레메트리에 스키마 URL 을 넣고 — Tracer·Meter·Logger 를 얻을 때의 schema_url 인자가 그 자리입니다 — 소비자는 받은 스키마 버전을 보고 필요하면 자기가 기대하는 버전으로 변환합니다. 문서의 예는 1.2.0 의 deployment.environment 를 1.1.0 을 기대하는 백엔드가 environment 로 바꿔 저장하는 것이고, 백엔드가 스키마를 모르면 Collector 의 스키마 변환 프로세서가 대신 합니다. OpenTelemetry 자체 스키마는 /schemas/<version> 경로로 발행됩니다.

현장에서 만나는 모습

한 팀이 "현재 활성 연결 수" 를 Counter 로 기록해서 그래프가 오르기만 했습니다. 연결이 닫힐 때 줄어야 하니 UpDownCounter 가 맞았고, 실제로는 연결 풀에서 값을 읽는 콜백이 더 자연스러워 Asynchronous UpDownCounter 로 바꿨습니다.

다른 팀은 Prometheus 로 가는 메트릭과 벤더 백엔드로 가는 메트릭의 값이 다르다고 했는데, 한쪽은 cumulative, 다른 쪽은 delta 로 설정되어 있었습니다. 같은 Counter 라도 temporality 가 다르면 한 점의 숫자가 뜻하는 구간이 다릅니다.

다음 퀴즈에서 확인할 것

퀴즈에서는 API 와 SDK 를 분리한 이유, Batching processor 의 기본값, 동기·비동기 계측기의 차이와 Context 연결, delta 와 cumulative 의 뜻, LogRecord 의 Timestamp 와 ObservedTimestamp, 브리지 API 의 대상, 스키마 URL 의 역할을 묻습니다.