OpenTelemetry 시맨틱 컨벤션 정리: 속성 이름을 정해 두는 이유

OpenTelemetry 시맨틱 컨벤션 정리: 속성 이름을 정해 두는 이유

지금까지 이 시리즈에서 속성 이름을 꽤 여러 개 지었습니다. order.id, db.system, http.request.method 같은 것들이요. 별생각 없이 지은 이름도 있고 어디서 본 듯한 이름도 있었죠.

그런데 이름은 정말 아무래도 괜찮을까요? 🤔 스팬 하나만 놓고 보면 그렇습니다. http.method라고 적든 request_method라고 적든 내가 읽으면 되니까요. 문제는 그 스팬이 나 혼자 만든 게 아닐 때 시작됩니다.

이름이 갈리면 잃는 것

상황을 그려 보겠습니다. 주문 서비스는 직접 계측해서 http.method를 씁니다. 결제 서비스는 자동 계측 라이브러리를 붙였는데 그쪽은 http.request.method를 쓰고요. 정산 서비스는 다른 팀이 만들어서 method라고만 적었습니다.

이제 “전체 서비스의 POST 요청 오류율”을 보는 대시보드를 만들어 봅시다. 조건을 세 번 다르게 써야 합니다. 알림 규칙도 세 벌이 되고요. 서비스가 하나 늘 때마다 그 서비스가 무슨 이름을 쓰는지 확인해서 조건을 추가해야 합니다.

OpenTelemetry 정리에서 계측 코드가 백엔드에 묶이지 않게 됐다고 했는데, 이름이 제각각이면 그 자유가 반쯤 무의미해집니다. 데이터는 한곳에 모였지만 같은 것으로 다뤄지지 않으니까요.

시맨틱 컨벤션(semantic convention)은 이 문제를 이름을 미리 정해 두는 방식으로 풉니다. HTTP 요청의 메서드는 http.request.method, 경로 패턴은 http.route, 응답 코드는 http.response.status_code라고 못 박아 두는 거죠. 그러면 누가 계측했든 같은 질의가 통합니다.

이름의 생김새

규약의 이름에는 규칙이 있습니다. 점으로 구분한 네임스페이스를 앞에 두고 뒤로 갈수록 구체적으로 적어요.

속성뜻
http.request.methodHTTP 메서드
http.route경로 패턴
http.response.status_code응답 상태 코드
url.path요청 경로
server.address접속한 서버 주소
db.system.name데이터베이스 종류
error.type오류 분류

http, url, server, db처럼 앞자리가 영역을 나눕니다. 그래서 db.로 시작하는 속성만 골라 보는 식의 질의가 가능하고, 새 영역이 생겨도 기존 이름과 부딪히지 않아요.

값의 형태까지 정해 둔 경우도 많습니다. http.request.method는 대문자로 적고 목록에 없는 메서드는 _OTHER로 뭉치라는 식이죠. 값이 제각각이면 이름만 맞춰도 소용이 없으니까요.

리소스 속성은 자리가 다르다

속성 중에는 스팬마다 달라지는 게 있고, 그 프로세스에서 언제나 같은 게 있습니다. http.route는 요청마다 다르지만 service.name은 이 프로세스가 사는 동안 바뀌지 않죠.

후자를 리소스(resource) 속성이라고 하고, 개별 신호가 아니라 프로세스에 한 번 붙입니다. 시리즈 내내 써 온 설정이 바로 그거였어요.

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "orders" }),
  // ...
});

리소스에 넣으면 그 프로세스가 내보내는 추적, 지표, 로그 전부에 같은 값이 붙습니다. 신호마다 따로 적을 필요가 없고 데이터 양도 줄죠. 서비스 이름, 버전, 배포 환경, 호스트 정보가 여기 들어갑니다.

여기에 수집기 정리에서 본 처리기가 겹칩니다. 리소스 속성은 애플리케이션에서 넣어도 되고 수집기에서 붙여도 되는데, 그 자리에서만 알 수 있는 정보인지로 나누면 깔끔합니다. 서비스 이름과 버전은 애플리케이션이 알고, 어느 노드에서 도는지는 수집기가 더 잘 알거든요.

안정된 것과 아직인 것

규약은 한 번에 완성되지 않았습니다. HTTP처럼 오래 논의된 영역은 굳었고, 새로 들어온 영역은 아직 바뀝니다.

이 차이가 패키지 구조에 그대로 드러나요. 안정된 이름은 기본 진입점에, 아직 바뀔 수 있는 이름은 incubating 진입점에 따로 들어 있습니다.

// 안정: 이름이 바뀌지 않는다고 약속된 것
import { ATTR_HTTP_ROUTE, ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";

// 미안정: 아직 바뀔 수 있는 것
import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from "@opentelemetry/semantic-conventions/incubating";

문자열을 직접 적지 않고 이 상수를 쓰면 이름이 바뀔 때 알아챌 수 있습니다. 미안정 쪽을 썼다면 패키지를 올릴 때 컴파일이 깨지면서 드러나거든요.

실제로 이름은 바뀝니다. 이 시리즈에서 쓴 것 중에도 있어요. 예전에는 http.method였던 게 지금은 http.request.method고, db.system도 db.system.name으로 옮겨 갔습니다. 앞의 글에서 db.system이라고 적은 예제는 지금 기준으로는 옛 이름인 셈이죠.

그래서 규약을 따른다는 건 한 번 맞추고 끝내는 일이 아니라 따라가는 일입니다. 다행히 백엔드가 옛 이름과 새 이름을 한동안 함께 다뤄 주고, 수집기의 처리기로 옛 이름을 새 이름으로 바꿔 주는 것도 가능합니다.

맞춰서 다시 써 보기

추적 정리에서 만든 스팬을 규약에 맞춰 손봐 보겠습니다. 문자열 대신 상수를 가져다 씁니다.

server.ts
import {
  ATTR_HTTP_REQUEST_METHOD,
  ATTR_HTTP_RESPONSE_STATUS_CODE,
  ATTR_HTTP_ROUTE,
  ATTR_URL_PATH,
} from "@opentelemetry/semantic-conventions";

tracer.startActiveSpan("GET /orders/:id", (span) => {
  span.setAttributes({
    [ATTR_HTTP_REQUEST_METHOD]: request.method,
    [ATTR_HTTP_ROUTE]: "/orders/:id",
    [ATTR_URL_PATH]: url.pathname,
    [ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
    "daleseo.order.id": url.pathname.split("/").at(-1), // 우리 것
  });
  span.end();
  return new Response("ok");
});

찍힌 속성입니다.

결과
attributes: {
  "http.request.method": "GET",
  "http.route": "/orders/:id",
  "url.path": "/orders/91",
  "http.response.status_code": 200,
  "daleseo.order.id": "91",
}

규약 속성과 우리 속성이 나란히 있지만 앞자리만 봐도 구분됩니다. 이 스팬은 이제 자동 계측이 만든 스팬과 같은 질의에 걸리고, 주문 번호는 우리만 아는 자리에 안전하게 남아 있어요.

ATTR_HTTP_ROUTE에 실제 경로가 아니라 패턴을 넣은 것도 규약이 정해 둔 내용입니다. 앞에서 스팬 이름에 값을 넣지 말라고 한 것과 같은 이유고, 규약이 그 판단을 대신 내려 준 셈이죠. 이름을 정해 둔다는 건 이렇게 자잘한 결정을 매번 다시 하지 않아도 된다는 뜻이기도 합니다.

직접 지어야 할 때

규약에 없는 것도 당연히 있습니다. order.id가 그렇죠. 주문이라는 개념은 우리 도메인의 것이니 표준에 있을 리가 없습니다.

이때 규칙은 둘입니다. 우선 우리 것임을 알 수 있는 네임스페이스를 앞에 둡니다. order.id보다는 회사나 제품 이름을 앞세운 형태가 안전해요. 규약이 나중에 order 영역을 정의하면 부딪히니까요.

그리고 규약이 이미 정한 이름은 다른 뜻으로 쓰지 않습니다. http.route에 경로 패턴이 아닌 걸 넣으면 그 속성을 아는 도구가 전부 오해합니다. 아예 새 이름을 만드는 게 낫죠.

카디널리티도 여기서 다시 등장합니다. 지표 정리에서 본 것처럼 지표 속성에 주문 번호를 넣으면 시계열이 폭발해요. 같은 order.id라도 스팬에는 괜찮고 지표에는 안 됩니다. 시계열이 왜 그렇게 불어나는지, 터졌을 때 어떻게 찾아서 줄이는지는 지표 카디널리티 정리에서 이어서 다룹니다. 이름을 정하는 것과 어디에 붙일지를 정하는 건 별개의 판단입니다.

규약이 넓어지는 방향

초기 규약은 HTTP, 데이터베이스, 메시징처럼 인프라 영역이 중심이었습니다. 그런데 관측해야 할 대상이 바뀌면 규약도 따라 넓어집니다.

최근에 들어온 영역이 생성형 AI입니다. 모델을 부르는 코드가 늘면서 “어떤 모델을 얼마나 썼나”를 재야 할 일이 생겼거든요. 그래서 gen_ai 네임스페이스가 만들어졌습니다.

속성뜻
gen_ai.provider.name모델 제공자
gen_ai.request.model요청한 모델 이름
gen_ai.operation.name작업 종류
gen_ai.usage.input_tokens입력 토큰 수

이름을 정해 두는 값어치가 이 영역에서 특히 큽니다. 여러 제공자를 섞어 쓰는 게 흔한데, 제공자마다 다른 이름으로 토큰 수를 기록하면 “이번 달 전체 토큰 비용”을 한 질의로 뽑을 수 없으니까요. 규약을 따르면 제공자를 바꿔도 대시보드는 그대로입니다.

이쪽은 아직 incubating에 있어서 이름이 더 바뀔 수 있습니다. 그래도 직접 짓는 것보다는 규약을 따라가는 쪽이 낫습니다. 나중에 도구가 이 이름을 알아보기 시작하면 아무것도 안 해도 화면이 생기거든요.

마치며

시맨틱 컨벤션은 화려한 기능이 아니라 이름표를 맞춰 두는 약속입니다. 그런데 이게 없으면 계측을 표준화한 의미가 반으로 줄어요. 데이터가 한곳에 모여도 같은 것으로 다뤄지지 않으니까요. 상수를 가져다 쓰고, 안정과 미안정을 구분하고, 직접 지을 때는 우리 네임스페이스를 앞세우는 것까지가 실천 사항입니다.

일곱 편에 걸쳐 OpenTelemetry를 봤습니다. 왜 계측을 백엔드에서 떼어 냈는지에서 시작해 스팬을 만들고, 서비스 경계를 넘기고, 수집기를 세우고, 지표와 로그를 붙이고, 이름을 맞추는 데까지 왔네요. 전부 하나씩 도입해도 되는 것들이라 오늘 당장은 추적 하나만 붙여 봐도 충분합니다.

규약의 전체 목록과 각 영역의 안정성 상태는 OpenTelemetry 공식 문서의 Semantic Conventions에서 확인할 수 있습니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord