OpenTelemetry/번역

[OTLP Spec] 1.0.0

justbagmeg 2024. 1. 3. 22:52
 

OTLP Specification 1.0.0

The OpenTelemetry Project Site

opentelemetry.io

OpenTelemetry Protocol(OTLP) 명세는 텔레메트리 소스, 중간 노드(예: 컬렉터)와 텔레메트리 백엔드 간의 텔레메트리 데이터의 인코딩, 전송 그리고 전달 메커니즘을 대해 설명한다.

(전송과 전달 메커니즘의 차이는 뭘까? 전송은 데이터가 어떻게 이동하는지에 관한 것으로 데이터가 소스(앱, 서비스)에서 다른 노드(컬렉터)로 이동하는 방법을 정의한다. 여기에는 네트워크 프로토콜(http, grpc)과 데이터 형식(json, proto)을 포함할 수 있다.

 

OTLP는 오픈텔레메트리 프로젝트 범위 내에서 디자인(설계)된 범용 목적의 텔레메트리 데이터 전달 프로토콜이다.

 

프로토콜 세부사항

OTLP는 텔레메트리 데이터의 인코딩과 클라이언트와 서버 사이의 데이터 교환에  사용되는 프로토콜을 정의한다.

 

이 명세는 OTLP가 gRPC 및 HTTP 1.1 전송을 사용해 어떻게 구현되는지 정의하고 페이로드(전송되는 데이터)에 사용되는 Protocol Buffer 스키마를 명시한다.

 

OTLP는 요청/응답 스타일의 프로토콜로 클라이언트가 요청을 보내면 서버는 해당하는 응답을 보낸다. 이 문서는 하나의 용청 및 응답 유형인 Export를 정의한다.

 

모든 서버 컴포넌트(구성 요소)는 반드시 다음과 같은 전송 압축 옵션을 지원해야 한다.

- 압축 없음. 'none'으로 표시된다.

- Gzip 압축. 'gzip'으로 표시된다.

OTLP/gRPC

gRPC 전송 채널을 설정한 후, 클라이언트는 ExportServiceRequest 메시지를 사용해 단일 요청으로 텔레메트리 데이터를 전송하기 시작한다. (로그의 경우 ExportLogsServiceRequest, 메트릭의 경우 ExportMetricsServiceRequest, 트레이스의 경우 ExportTraceServiceRequest 사용). 클라이언트는 지속적으로 일련의 요청을 서버로 보내고 각 요청에 대한 응답을 받기를 기대한다.

https://opentelemetry.io/docs/specs/otlp/img/otlp-request-response.png

참고: 이 프로토콜은 한 쌍의 클라이언트/서버 노드 간의 전달 신뢰성에 초점을 맞추고 있으며 클라이언트와 서버 사이의 전송 과정에서 데이터가 손실되지 않도록 보장하는 것을 목표로 한다. 많은 텔레메트리 수집 시스템은 데이터가 최종 목적지에 도달하기까지 거쳐야 하는 중간 노드가 있다 (예: 앱 -> 에이전트 -> 컬렉터 -> 백엔드). 이러한 시스템에서 End-to-End(종단) 간 전달 보장은 OTLP의 범위를 벗어난다. 이 프로토콜에서 설명하는 ACK는 단일 클라이언트/서버 사이에 발생하며, 다중 홉(여러 단계에 걸친) 전달 경로에서의 중단 노드는 포함하지 않는다.

 

OTLP/gRPC Concurrent Requests(동시 요청)

요청을 보낸 후 클라이언트는 서버로부터 응답을 받기까지 기다릴 수 있다. 이 경우 서버로부터 확인 응답(ACK)을 받지 못한 요청은 최대 한 개만 존재한다.

https://opentelemetry.io/docs/specs/otlp/img/otlp-sequential.png

단순한 구현을 원하거나 클라이언트와 서버가 매우 낮은 지연 시간을 가진 네트워크를 통해 연결되어 있을 때, 예를 들어 클라이언트가 계측되는(instrumented) 애플리케이션이고 서버가 로컬 데몬으로 실행되는 OTEL collector일 경우 순차적인 작업이 권장된다.

 

높은 처리량을 달성해야 하는 구현은  더 높은 처리량을 달성하기 위해 동시 단일 호출을 지원해야 한다. 클라이언트는 이전에 보내 요청에 대한 응답을 기다리지 않고 새로운 요청을 보내야 하며, 사실상 서버에 의해 아직 확인 응답을 받지 못한 요청들의 파이프라인을 생성한다.

https://opentelemetry.io/docs/specs/otlp/img/otlp-concurrent.png

동시 요청의 수는 설정할 수 있어야 한다.

 

최대로 달성 가능한 처리량은  max_concurrent_request * max_request_size / (network_latency + server_response_time) 이 된다. 예를 들어, 요청이 최대 100개의 스팬을 포함할 수 있고, 네트워크 라운드 트립 지연 시간이 200ms이고, 서버 응답 시간이 300ms 라면 한 개의 동시 요청으로 달성 가능한 최대 처리량은  100 spans / (200ms + 300ms) 또는 초당 200 spans이다. 높은 지연 시간의 네트워크나 서버 응답 시간이 견 경우에 높은 처리량을 달성하기 위해서는 요청이 매우 커지거나 많은 수의 동시 요청이 이뤄져야 함을 쉽게 알 수 있다.

 

만약 클라이언트가 종료 중일 떄 클라이언트는 모든 보류 중인 확인 응답을 받거나 implementation-specific 타임아웃이 만료될 때까지 선택적으로 기다릴 수 있다. 이는 텔레메트리 데이터의 신뢰 있는 전달을 보장한다. 클라이언트 구현은 종료 중 대기 여부를 on/off 할 수 있는 옵션을 제공해야 한다.

 

만약 클라이언트가 특정 요청을 전달할 수 없다면(대기 중 타이머 만료), 클라이언트는 데이터가 전달되지 않았다는 사실을 기록해야 한다.

 

OTLP/gRPC Response

응답은 반드시 적절한 메시지여야 한다. (Full Success, Partial Success 그리고 Failure cases에 사용할 메시지는 아래에서 확인)

 

Full Success

성공 응답은 텔레메트리 데이터가 성공적으로 서버에서 처리됐음을 나타낸다.  (서버에 의해 수락됐다?)

 

만약 서버거 비어있는 요청(텔레메트리 데이터가 없는 경우)을 받으면, 서버는 성공 상태로 응답해야 한다. 

 

성공적으로 요청을 처리한 경우, 서버는 반드시 Export<signal>ServiceResponse 어야 한다. (트레이스는 ExportTraceServiceResponse, 매트릭은 ExportMetricsServiceResponse 그리고 로그는 ExportLogsServiceResponse)

 

성공 응답일 경우 서버는 반드시 partial_success 필드를 설정하지 않아야 한다.

 

Partial Success

만약 요청이 일부분만 처리됐다면(서버가 데이터의 일부분만 처리하고 나머지는 처리하지 않았다면), 서버의 응답은 반드시 Full Success의 경우와 마찬가지로 Export<signal>SerivceReponse 메시지여야 한다.

 

또, 서버는 반드시 partial_success필드를 초기화하고( 트레이스의 경우 ExportTracePartialSuccess 메시지, 메트릭의 경우 ExportMetricsPartialSuccess 메시지, 로그의 경우 ExportLogsPartialSuccess 메시지 ) rejected_spans, rejected_data_points, rejected_log_records 필드를 서버가 처리하지 못한(거부한 rejected) spans / data points / log 의 수로 설정해야 한다. 

 

서버는 error_message 필드를 사람이 읽을 수 있는 영어 에러 메시지로 채워야 한다. 메시지는 왜 서버가 왜 데이터 일부를 처리하지 못했는지(거절했는지) 이유를 설명해야하고 사용자가 해당 문제를 어떻게 해결할 수 있을지 가이드를 제공할 수 있다. 프로토콜은 에러 메시지의 구조를 정의하려고 하지 않는다.

 

서버는 요청을 모두 처리했더라도 partial_success 필드를 이용해 클라이언트에 경고 / 제안을 전달하기 위해 사용할 수 있다. 이러한 경우에는 rejected_<signal> 필드는 반드시 0의 값을 가져야 하며, error_message 필드는 비어 있지 않아야 한다.

 

클라이언트는 parital_success가 채워진 일부 성공 응답을 받은 경우 요청을 다시 시도해서는 안된다.

 

Failures

서버가 에러를 반환할때, 재시도 가능한(retryable) 오류와 재시도 불가능한 오류 2개의 큰 카테고리로 나뉜다. 

  • 재시도 가능한 에러는 데이터 처리가 실패했음을 나타내고, 클라이언트는 에러를 기록하고 동일한 데이터를 재전송 시도할 수 있다. 예를 들어, 이는 서버가 일시적으로 데이터를 처리하지 못할 때 발생할 수 있다.
  • 재시도 불가능한 에러는 데이터 처리가 실패했음을 나타내고, 클라이언트는 동일한 데이터의 재전송을 시도해서는 안된다. 클라이언트는 반드시 텔레메트리 데이터를 폐기해야 한다. 예를 들어, 이는 요청이 서버에서 deserialize나 처리할 수 없는 bad data인 경우 발생할 수 있다. 클라이언트는 폐기된 데이터의 개수를 기록(유지) 해야 한다.

 

서버는 반드시 Unavailable을 이용해 재시도 가능한 에러를 나타내야하며, 값이 0인 RetryDelay를 포함하는 RetryInfo를 이용해 status, 추가적인 세부사항을 제공할 수 있다. 예시 코드는 다음과 같다.

// Do this on server side.
  st, err := status.New(codes.Unavailable, "Server is unavailable").
    WithDetails(&errdetails.RetryInfo{RetryDelay: &duration.Duration{Seconds: 0}})
  if err != nil {
    log.Fatal(err)
  }

  return st.Err()

 

재시도 불가능한 에러를 나타내기 위해 InvalidArgument를 사용하는 것이 권장되며, BadRequest를 이용한 status를 통해 추가적인 세부사항을 제공할 수 있다.  서버가 더 적절한(상황에 맞는) gRPC 상태 코드를 선택할 수 있다면 해당 코드를 사용할 수 있다. 이를 설명하기 위한 Go 코드의 일부는 다음과 같다.

// Do this on the server side.
  st, err := status.New(codes.InvalidArgument, "Invalid Argument").
    WithDetails(&errdetails.BadRequest{})
  if err != nil {
    log.Fatal(err)
  }

  return st.Err()

서버는 특정 에러 상황에 더 적합한 gRPC 코드가 있다면 다른 gRPC 코드를 이용해 재시도 가능한, 재시도 불가능한 에러를 나타낼 수 있다.  클라이언트는 다음 표에 따라 gRPC 상태 코드를 재시도 가능한지 또는 재시도 불가능한지를 판단(구분) 해야 한다.

gRPC Code Retryable?
CANCELLED Yes
UNKNOWN No
INVALID_ARGUMENT No
DEADLINE_EXCEEDED Yes
NOT_FOUND No
ALREADY_EXISTS No
PERMISSION_DENIED No
UNAUTHENTICATED No
RESOURCE_EXHAUSTED Only if the server can recover (see below)
FAILED_PRECONDITION No
ABORTED Yes
OUT_OF_RANGE Yes
UNIMPLEMENTED No
INTERNAL No
UNAVAILABLE Yes
DATA_LOSS Yes

재시도할 때, 클라이언트는 Backoff 전략을 구현해야 한다. 이에 대한 예외 사항은 아래 설명된 Throttling의 경우로, 재시도 간격에 대한 명시적인 지침을 제공한다.

클라이언트는 서버가 리소스 고갈(exhaust)로부터 회복이 가능하다고 시그널을 보내는 경우에만 RESOURCE_EXHAUSETED 코드를 재시도 간으한 것으로 판단해야 한다. 이는 서버가 RetryInfo를 포함하는 status를 반환해서 시그널을 보낸다. 이 경우 서버와 클라이언트의 동작(행동)은 OTLP/gRPC Throttling 섹션에 설명된 것과 정확히 같다. 만약 RetryInfo를 포함하는 status가 반환되지 않으면, RESOURCE_EXHAUSETED 코드는 재시작 불가능한 것으로 처리되어야 한다.

 OTLP/gRPC Throttling

OTLP는 백프레셔 신호를 허용한다.

백프레셔 신호란? 시스템이 과부하 상태에 있을 때 이를 다른 구성 요소나 시스템에 알리는 것. OTLP 맥락에서는 서버가 처리할 수 있는 양을 초과하는 데이터가 전송되고 있을 때, 이를 클라이언트에 알려 전송 속도를 줄이거나 일시적으로 전송을 중단해 서버의 부하를 줄일 수 있다.

만약 서버가 클라이언트로부터 받는 데이터의 속도를 감당할 수 없는 경우, 서버는 이 사실을 클라이언트에게 신호를 보내야 한다. 그러면 클라이언트는 서버에 과부하를 주지 않기 위해 자신의 전송 속도를 조절해야 한다.

 

gRPC 전송 사용 시 백프레셔 시그널을 보내기 위해 서버는 반드시 Unavailable 코드를 포함한 에러를 반환해야 하며, RetryInfo를 사용해 status를 통해 추가적인 세부사항을 제공해야 한다. 여기 간단한 Go 코드 예시가 있다.

// Do this on the server side.
  st, err := status.New(codes.Unavailable, "Server is unavailable").
    WithDetails(&errdetails.RetryInfo{RetryDelay: &duration.Duration{Seconds: 30}})
  if err != nil {
    log.Fatal(err)
  }

  return st.Err()

  ...

  // Do this on the client side.
  st := status.Convert(err)
  for _, detail := range st.Details() {
    switch t := detail.(type) {
    case *errdetails.RetryInfo:
      if t.RetryDelay.Seconds > 0 || t.RetryDelay.Nanos > 0 {
        // Wait before retrying.
      }
    }
  }

클라이언트가 이 신호를 받으면, RetryInfo에 대한 문서에 설명된 권장 사항을 따라야 한다.

// Describes when the clients can retry a failed request. Clients could ignore
// the recommendation here or retry when this information is missing from the error
// responses.
//
// It's always recommended that clients should use exponential backoff when
// retrying.
//
// Clients should wait until `retry_delay` amount of time has passed since
// receiving the error response before retrying. If retrying requests also
// fail, clients should use an exponential backoff scheme to increase gradually
// the delay between retries based on `retry_delay` until either a maximum
// number of retries has been reached, or a maximum retry delay cap has been
// reached.
클라이언트가 실패한 요청을 언제 재시도할 수 있는지에 대해 설명한다. 클라이언트는 여기에 제시된 권장사항을 무시하거나 에러 응답에서 이 정보가 누락됐을 때 재시도할 수 있다.

클라이언트는 재시도할 때 항상 expotential(지수) 백오프 하는 것이 권장 된다. (지수 백오프는 재시도 사이의 대기 시간을 각 시도 후에 지수적으로 증가시키는 것. 예를 들어 첫 시도 후 대기 시간이 1초, 다음 대기 시간은 2초, 그 다음은 4초... 8초 ... 16초 처럼 대기 시간이 늘어난다.)

클라이언트는 에러 응답을 받은 후 재시도 전까지 'retry_delay' 만큼의 시간을 기다려야 한다. 만약 재시도 요청 또한 실패한다면, 클라이언트는 'retry_delay' 기반으로 점진적으로 재시도 간격을 지수 백오프 방식으로 늘려야 하며,최대 재시도 횟수에 도달하거나 최대 재시도 시간 한도에 도달할 때까지 계속된다. 

 

'retry_delay'의 값은 서버에 의해 결정되며 구현에 따라 다르다. 서버는 서버가 회복하기에 충분하면서 클라이언트가 데이터 전송량을 조절하던 데이터를 폐기하지 않을 정도로 크지 않은 'retry_delay' 값을 선택해야 한다.

OTLP/gRPC Service and Protobuf Definitions

gRPC 서비스 정의는 여기에서 확인.

요청/응답을 위한 Protobuf는 여기에서 확인.

프로토 버전과 배포 상태(?)를 확인하자. 다양한 signal에 대한 스키마는 배포 수준이 다르며, 일부는 stable하고 일부는 베타 단계에 있을 수 있다.

OTLP/gRPC Default Port

OTLP/gRPC 네트워크의 기본 포트는 4317 이다.

 

OTLP/HTTP

OTLP/HTTP는 이진 포맷이나 JSON 포맷으로 인코딩 된 Protobuf 데이터를 사용한다. 인코딩에 관계없이 메시지의 Protobuf 스키마는 여기에 정의된 것과 같이 OTLP/HTTP나 OTLP/gRPC나 같다.

OTLP/HTTP는 텔레메트리 데이터를 클라이언트에서 서버로 전송하기 위해 HTTP POST 요청을 사용한다. 구현체는 HTTP/1.1 또는 HTTP/2 프로토콜을 사용할 수 있으며, HTTP/2 프로토콜을 사용하는 경우 HTTP/2 연결을 맺을 수 없으면 HTTP/1.1 프로토콜로 전환해야 한다.

Binary Protobuf Encoding

이진 포맷으로 인코딩 된 Protobuf 데이터는 proto3 인코딩 표준을 따른다. 

클라이언트와 서버는 이진 포맷으로 인코딩 된 Protobuf 데이터를 보낼때는 반드시 요청과 응답의 헤더에 "Content-Type:application/x-protobuf"를 설정해야 한다.

JSON Protobuf Encoding

JSON 으로 인코딩된 Protobuf 데이터는 Protobuf와 JSON 사이의 매핑을 위해 proto3 표준에 정의된 JSON 매핑을 사용하며 다음과 같은 차이가 있다.

  • 'traceId'와 'spanId' 바이트 배열은 대소문자를 구분하지 앟는 16진수로 인코딩된 문자열로 표현된다. 표준 Protobuf JSON 매핑에 정의된 것처럼 base64로 인코딩 되지 않는다. 'traceId'와 'spanId' 필드는 OTLP Protobuf 메시지 전체에서 16진수 인코딩을 사용하며, Span, Link, LogRecord 와 같은 메시지가 있다. 예를 들어, Span의 'traceId' 필드는 다음과 같이 표현될 수 있다: {"traceId": " 5B8EFFF798038103D269B633813FC60C", ...}
  • enum(열거형) 필드의 값은 반드시 정수 값으로 인코딩돼야 한다. 열거형 필드가 정수 또는 열거형 이름 문자열로 인코딩 될 수 있는 표준 Protobuf JSON 매핑과 달리 OTLP JSON Protobuf 인코딩에서는 정수형 열거값만 가능하며 열거형 문자열 이름은 사용돼서는 안된다. 예를 들어, Span에서 SPAN_KIND_SERVER 값을 가진 'kind' 필드는 다음과 같이 표현될 수 있다: {"kind": 2, ..}
  • OTLP/JSON 리시버는 알 수 없는(알려지지 않은) 이름의 필드는 무시해야 하며 이런 필드가 페이로드(데이터)에 존재하지 않는 것처럼 언마샬(unmarshal)해야 한다. 이는 이진 포맷 Protobuf unmarshaler의 동작과 일치하며, OTLP 메시지에 새로운 필드를 추가해도 기존 리시버가 중단되지 않도록 보장한다.
  • JSON 객체의 키는 필드 이름을 lowerCamelCase로 변환한 것이다. 원본 필드 이름은 JSON 객체의 키로 사용할 수 없다. 예를 들어, 다음은 유효한 Resour의 JSON 표현이다: {"attribute": {...}, "droppedAttributesCount": 123}, 그리고 다음은 유효하지 않은 표현이다: {"attritbutes": {...}, "dropped_attributes_count": 123}.

Protobuf 사양에 따르면, JSON으로 인코딩된 페이로드에서 64비트 정수 숫자는 십진수 문자열로 인코딩되며, 디코딩할 때 숫자나 문자열 모두 허용된다.

클라이언트와 서버는 JSON Protobuf 인코딩 페이로드를 전송할 때 반드시 요청 및 응답 헤더에 "Content-Type: application/json"을 설정해야 한다.


마지막 수정일: 2024년 03월 05일

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'OpenTelemetry > 번역' 카테고리의 다른 글

[Specs] - OTel 1.30.0 / Overview  (0) 2024.04.14
[Concepts] Instrumentation / Code-base  (0) 2024.04.14
[Concepts] Observability Primer  (0) 2024.04.14
[Demo] Architecture  (0) 2024.04.12
[Specification] Common specification concepts  (1) 2023.11.23