🐌 학습노트/낙서장

[모니터링의 새로운 미래 관측가능성 #5] 6장. 관측가능성의 표준, 오픈텔레메트리

mini_world 2024. 4. 14. 16:43
목차 접기

📌 이 내용은 책 내용 메모입니다.
개인적인 생각과 경험 그리고 여러 잡담이 있으니, 책을 읽으며 의견을 나누고싶은분이 봐주시면 좋겠습니다.

6.1 오픈텔레메트리 소개

OpenTelemetry는 한마디로 정리하자면 로그, 메트릭, 추적을 한번에 수집하는 에이전트 역할을 한다고 말할 수 있다.

역사

OpenTelemetry는 Cloud Native Computing Foundation (CNCF) 프로젝트로, 이전의 두 프로젝트인 OpenTracingOpenCensus가 합병하여 만들어졌다.

OpenTracing, OpenCensus는 코드를 계측하고 관측성 백엔드로 텔레메트리 데이터를 보내는 표준이 없었기 때문에 두 프로젝트의 핵심 기능을 통합하고 텔레메트리 데이터의 생성,수집,전송 방식을 표준화하는것을 목표로 하고있다.

👉 OpenTelemetry Mission 보러가기

아키텍쳐

https://opentelemetry.io/docs/

OpenTelemetry는 업계표준으로, CNCF내에서도 큰 영향력을 미치고 있다. (OpenTelemetry Venders)

OpenTelemetry는 백엔드 시스템을 구현하는 것이 아니라, 어플리케이션에서 관측 데이터를 수집하고 다양한 백엔드 시스템으로 텔레메트리 데이터를 보내는 방법에 대한 표준을 제공한다.

목표

OpenTelemetry는 추적,지표, 로그 상관 관계를 연결하는것이 주 목표다.

https://opentelemetry.io/docs/specs/otel/logs/

6.2 오픈텔레메트리 컴포넌트

오픈텔레메트리의 핵심 개념은 Signals(신호), Context Propagation(컨텍스트전파) 그리고 Pipeline(파이프라인)이다.

OpenTelemetry
├── Signals
│   ├── Trace
│   ├── Metrics
│   ├── Log
│   └── Baggage
├── Context Propagation 
└── Pipeline
  • Signals (신호): 추적, 메트릭, 로그, 배기지 등의 텔레메트리 데이터 유형에 대한 기술적 스펙을 정의한다.
    신호는 추적,메트릭,로그,배기지를 포괄하는 개념이다.
    • **Signals에서 정의하는것**
      • 신호에 대한 가이드라인을 제공하는 스팩
      • 신호 표현방식을 나타내는 데이터 모델
      • 어플리케이션과 라이브러리 개발자가 코드를 계측하는데 사용할 수 있는 API
      • 사용자가 API를 사용하여 텔레메트리를 생성할 수 있도록 하는데 필요한 SDK
      • 사용을 단순화 하는 계측 라이브러리
      • 계측을 단순화 하는 라이브러리와 에이전트
    • **Signals에 포함되는것**
      • Trace (추적): 시스템 요청 및 트랜잭션의 이동 정보
      • Metrics (메트릭): 시스템 수치적 성능 지표
      • Log (로그): 이벤트 메시지
      • Baggage (배기지): 서비스 간 요청과 함께 전달되는 메타데이터 컨텍스트 정보
  • Context Propagation (콘텍스트 전파): 서비스나 요청 간에 컨텍스트 정보(예: 보안 토큰, 사용자 세션 식별자 등)를 전달하는 방법을 정의한다. 업스트림과 다운스트림 서비스 간에 정보가 일관되게 유지되도록 보장한다.
  • Pipeline (파이프라인): 수집된 데이터를 처리하고 적절한 백엔드 시스템(예: 모니터링 도구, 분석 플랫폼 등)으로 전송하는 과정을 구현하는 방법을 정의한다.

6.2.1 Signals(신호) 구성요소

[1] 데이터 모델

데이터 모델은 메트릭, 추적, 로그에 대한 상세한 레이아웃과 데이터 구조를 기술한다. (OpenTelemetry Spec)
OpenTelemetry 데이터 모델은 메트릭,추적,로그간의 상관관계를 쉽게 구현할 수 있는 표준화 메세지와 데이터 형식이며, 벤더 중립적으로 설계되었다. gRPC 프로토콜 버퍼와 호환성에도 문제가 없다.

  • Metrics: https://opentelemetry.io/docs/specs/otel/overview/#metric-signal
    • 잘 만들어진 메트릭 솔루션(예,프로메테우스)이 많이 있다는 점을 감안하여, Opentelemetry는 메트릭을 기존 솔루션에 통합하는것에 중점을 두고 있다.
    • prometheus, StatsD에 완전히 호환된다. (참고)
  • Logs: https://opentelemetry.io/docs/specs/otel/overview/#log-signal
    • 로그는 기존 로그 시스템과의 호환성을 유지하면서 추적 정보와 같은 텔레메트리 데이터를 통합할 수 있도록 설계했다.
    • 기존 로그 메세지 형식에 추적 등의 상관관계 정보와 텔레메트리 정보를 추가하는것이 일반적인 오픈텔레메트리의 방향성이다.
  • Trace : https://opentelemetry.io/docs/specs/otel/overview/#tracing-signal
    • Trace는 기존의 다양한 추적 표준을 유지하는것보다는 새로운 오픈텔레메트리 추적으로 통합하는 방식을 사용했다.
    • Trace의 핵심은 Spen이며, Spen간의 상관관계를 나타내는것이 핵심이다.
  • Baggage: https://opentelemetry.io/docs/specs/otel/overview/#baggage-signal
    • 요청을 따라 이동하는 키-값 쌍의 집합을 의미하며, Baggage를 통해서비스간의 상태와 컨텍스트 정보를 전달하고 유지한다.
    • 원격 분석에 주석을 달고 메트릭, 추적 및 로그에 컨텍스트 정보를 추가하는 데 사용된다.

각 신호는 4가지 코어 컴포넌트를 포함하고있다.

  • APIs: 텔레메트리 데이터(추적, 메트릭, 로그)를 수집하기 위한 일련의 규칙과 메소드를 제공함
  • SDKs: OpenTelemetry 프로젝트에 의해 제공되는 API의 구현체, API에 정의된 명세를 실제로 실행하는 코드를 포함함 (SDK는 API에 의존적)
  • OpenTelemetry Protocol (OTLP)
  • Collector

[2] API

- 참고링크 (OpenTelemetry API)

API와 SDK가 이해되지 않아서, 매우 혼란스러웠는데, 코드 예제를 보면 조금이나마(?) 이해가 가능하다.

API는 추적,메트릭,로그 등의 데이터 수집을 위한 규격(명세)를 정의한다.
즉, API는 텔레메트리 데이터를 다루는 규칙이라고 생각할 수 있다.

# API만 사용하는 예제
# 실제로 데이터를 수집하고 처리하기 위해서는 SDK가 필요함

from opentelemetry import trace

# API를 통해 트레이서를 가져옴
tracer = trace.get_tracer(__name__)

# API를 사용하여 추적 시작
with tracer.start_as_current_span("api-only-span"):
    print("This span is created using API only and won't be exported without an SDK.")

[3] SDK

SDK는 API스펙을 가지고 각 프로그래밍 언어나 환경에서 실제 작업을 수행한다.
즉, SDK는 API명세에 따라 데이터를 수집,처리, 내보내기 등의 구체적인 코드를 포함하게 된다.
정리하자면, API는 이론적인 설계도이고, SDK는 설계도를 바탕으로 실제 건물을 짓는 건축업자라고 비유할 수 있다.

# API와 SDK를 함께 사용하는 코드 예제 (Console Spen Exporter SDK 사용)
# SDK는 API가 제공하는 인터페이스에 따라 데이터를 처리하고 내보낸다.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# SDK 설정: 트레이서 제공자 설정
trace.set_tracer_provider(TracerProvider())

# 콘솔 내보내기 설정
span_processor = SimpleSpanProcessor(ConsoleSpanExporter())
trace.get_tracer_provider().add_span_processor(span_processor)

# API와 SDK를 사용하여 추적 시작
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("api-and-sdk-span"):
    print("This span is created using both API and SDK and will be exported to the console.")

API와 SDK는 이해하기 어렵게 왜이렇게 해놨냐!!
이유는 유연성을 제공하기 위함이다. 동일한 API를 사용하면서 다양한 SDK를 선택하거나 교체할 수 있어, 특정 실행환경 및 요구사항에 더 적합한 SDK를 선택하거나 교체할 수 있다.
이 구조 덕분에 OpenTelemetry는 다양한 플랫폼과 언어에서 널리 사용될 수 있는것이다.

# 같은 API를 사용하지만, 다른 SDK를 사용하는 예제 (OTLP Span Exporter SDK사용)

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter

# SDK 설정: 트레이서 제공자 설정
trace.set_tracer_provider(TracerProvider())

# OTLP 내보내기 설정
otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
span_processor = SimpleSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# API와 SDK를 사용하여 추적 시작
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("api-and-sdk-span-otlp"):
    print("This span is created using both API and SDK and will be exported via OTLP.")

[4] 계측라이브러리 (instrumentation libraries)

계측라이브러리는 자동계측과 같은 의미라고 볼 수 있다.

웹 프레임워크(예: Flask, Django, Spring), 데이터베이스 클라이언트(예: JDBC, Sequelize), RPC 라이브러리(예: gRPC, Thrift) 등 에 대한 계측라이브러리가 있고, 각 언어별 라이브러리는 해당 언어의 특성과 환경에 맞게 설계되어있다.
API개발 없이 자동으로 계측이 가능하도록 미리 설계된 OpenTelemetry 라이브러리인것이다.

https://opentelemetry.io/ecosystem/registry/ 이 링크에서 계측라이브러리들을 찾아볼 수 있다.
(진척률이 다 다르므로 쓰기전에 확인해봐야함)

예시로, FastAPI는 opentelemetry-instrumentation-fastapi를 사용할 수 있다.

# opentelemetry-instrumentation-fastapi 를 사용하기 예제

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter

app = FastAPI()

# 트레이서 제공자 설정
trace.set_tracer_provider(TracerProvider())

# OTLP Exporter 설정
otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
span_processor = SimpleSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# FastAPI 앱에 대한 계측 설정
FastAPIInstrumentor.instrument_app(app)

@app.get("/")
async def hello():
    return {"message": "Hello from FastAPI with OpenTelemetry!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
# opentelemetry-instrumentation-fastapi 없이 fastapi 계측하기
# 미들웨어 기능을 사용하거나 경로연산자에 직접 추적코드 추가 (이 예제에서는 미들웨어 기능 사용)

from fastapi import FastAPI, Request
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter

app = FastAPI()

# 트레이서 제공자 설정
trace.set_tracer_provider(TracerProvider())

# OTLP Exporter 설정
otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
span_processor = SimpleSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# 트레이서 가져오기
tracer = trace.get_tracer(__name__)

# 미들웨어 추가
@app.middleware("http")
async def add_tracing(request: Request, call_next):
    with tracer.start_as_current_span(f"HTTP {request.method} {request.url.path}"):
        response = await call_next(request)
    return response

@app.get("/")
async def hello():
    return {"message": "Hello from FastAPI without automatic instrumentation!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

그냥 보기에는 비슷비슷한데..
직접 미들웨어를 구현하는것보다 opentelemetry-instrumentation-fastapi 를 사용하는게 무슨 이점이 있을까?

  • 간편한 통합: 수동으로 각 경로나 미들웨어를 설정할 필요 없이 바로 사용할 필요 없고, 계측로직을 신경쓸 필요가 없다.
  • 최적화: 계측 라이브러리는 해당 프레임워크의 내부 동작에 대한 깊은 이해를 바탕으로 개발된다. 직접 개발하는것보다 효율적이고 성능에 덜 영향을 미친다. 또한, 에러 추적, 비동기 지원 등에 대한 추가 기능도 간편하게 사용할 수 있다.
  • 활발한 커뮤니티: 유지보수나 업데이트 측면에서도 좋다. 지속적으로 업데이트 되고 유지보수 되기때문이다. OpenTelemetry 자체의 변화에 따라서 라이브러리도 업데이트 되기 때문이다.

[5] 리소스 (Resources)

- 참고: OpenTelemetry Resources

텔레메트리 데이터를 생성하는 엔티티(예: 애플리케이션, 서버, 서비스)를 설명하는 데 사용되는 메타데이터를 말한다.
특정 엔티티에 대한 추가 정보를 제공하여, 리소스를 쉽게 식별하고, 관리하며, 필터링할 수 있도록 한다. 비유하자면, AWS 의 Resource Tag, K8s의 Metadata 같은 역할이다.

OpenTelemetry는 추적을 초기화하기 위해 TracerProvider, 메트릭을 초기화 하기 위해 MetricProvider를 사용한다.
각 Provider 생성 시점에 리소스도 생성하며, Exporter에서 리소스를 참고한다.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# 리소스 생성
resource = Resource.create({
    "service.name": "ExampleService",
    "service.version": "1.0.0",
    "host.name": "example-host"
})

# 트레이서 제공자에 리소스 설정
trace.set_tracer_provider(TracerProvider(resource=resource))

# 추적 설정
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("example-operation"):
    print("This operation is associated with a resource.")

[5-추가] 어트리뷰트 (attributes)

코드를 확인하다보니, 태그처럼 사용할 수 있는게 리소스 외에도 어트리뷰트라는게 있는걸 알게되었다.
무엇이 다른건지 확인하고 넘어가보자

  • 어트리뷰트(Attributes)
    • 목적: 어트리뷰트는 개별 스팬(추적 데이터의 기본 단위)에 직접 연결되며, 그 스팬의 작업을 더 구체적으로 설명하기 위해 사용된다. 예를 들어, HTTP 요청을 처리하는 스팬에는 요청 메소드, URL, 상태 코드와 같은 정보가 어트리뷰트로 추가될 수 있다.
    • 적용 범위: 어트리뷰트는 특정 스팬에 국한되어 사용되며, 스팬마다 다를 수 있다. 이는 동적으로 변할 수 있는 정보를 포함하며, 스팬의 생명주기 동안에만 유효하다.
  • 리소스(Resources)
    • 목적: 리소스는 추적 데이터를 생성하는 엔티티(예: 애플리케이션, 서버, 서비스)에 대한 정보를 제공한다. 이는 추적 데이터가 생성된 환경에 대한 정적인 정보를 포함하며, 텔레메트리 데이터 전체에 일관되게 적용된다.
    • 적용 범위: 리소스는 트레이서 프로바이더나 메트릭 프로바이더와 연결되어, 해당 프로바이더를 사용해 생성된 모든 스팬이나 메트릭에 공통적으로 적용된다. 예를 들어, 하나의 서비스 이름, 서비스 버전, 호스트 이름 등이 모든 스팬에 걸쳐 일관적으로 표현되어야 할 정보다.

[6] 배기지 (Baggage)

- 링크: https://opentelemetry.io/docs/concepts/signals/baggage/

배기지는 추적에서 사용되는 사용자가 정의 가능한 키-값 쌍 형식의 메타데이터 이다.

from opentelemetry import trace
from opentelemetry import baggage

# 트레이서와 배기지 초기화
tracer = trace.get_tracer(__name__)

# 사용자 세션 정보를 배기지에 추가
baggage.set_baggage("user.session_id", "session_12345")
baggage.set_baggage("user.preferred_language", "en-US")

# 배기지를 활용한 추적 시작
with tracer.start_as_current_span("example-operation") as span:
    # 배기지에서 데이터를 추출하여 로그에 기록
    session_id = baggage.get_baggage("user.session_id")
    language = baggage.get_baggage("user.preferred_language")
    span.set_attribute("user.session_id", session_id)
    span.set_attribute("user.language", language)
    print(f"Operating in session {session_id} with language setting {language}")

배기지를 사용하는 목적은 분산 시스템에서 서비스 간 요청을 처리할 때 중요한 메타데이터를 전달하고 유지하는 것이다.
전체 트랜젝션의 맥락을 유지하면서 각 서비스가 필요한 정보에 접근할 수있도록 한다.

  • 필요성
    • 배기지는 콘텍스트 값을 통일된 형식과 패턴으로 제공하여, 모든 서비스가 동일한 정보를 공유하고 사용할 수 있도록 한다.
    • 언어나 플랫폼에 관계없이, 모든 응용 프로그램이 배기지를 읽고 구문 분석할 수 있어, 다양한 기술 스택을 사용하는 대규모 분산 시스템에서도 통합성을 유지할 수 있다.
    • 대규모 분산시스템을 구축하고 팀이 원하는 언어나 프레임워크를 사용할 수 있도록 자율성을 제공하려는 경우에 유용하다.
  • 활용법
    • 외부에 잠재적으로 노출해도 상관없는 민감하지 않는 데이터에 사용한다. (예, 계정ID, 사용자 ID, 제품 ID, 원본 IP등)
    • 상위 스택에서만 엑세스 할 수 있는 정보가 있다면 하위 스택으로 전달하기 위해서 배기지에 정의하고 다운스트림 서비스의 스팬에 전파할 수 있다.
    • 관측가능성 백엔드에서도 배기지를 사용하면 보다 쉬운 검색과 필터링이 가능하다.
  • 주의사항
    • 배기지는 분산 시스템 내 모든 서비스에 전달되기 때문에, 민감한 정보는 배기지를 통해 전송하지 않도록 해야 한다.
    • 각 시스템마다 (예, fastapi, spring, rails를 사용하고있다면 각각) 동일한 key-value 를 가진 배기지를 설정해주어야 한다.

[7] 이벤트

이벤트(Event) 는 추적(Trace) 중에 특정 스팬(Span)과 연관된 시간에 민감한 이벤트를 나타낸다.이벤트는 스팬 내에서 특정 순간에 발생하는 사건을 기록하여 추적 데이터에 추가적인 정보와 컨텍스트를 제공하는 데 사용한다.

기록된 이벤트들은 스팬(Span)과 함께 수집되어 추적 데이터의 일부로 관측 가능성 백엔드 시스템에 전송된다.
스팬의 생명주기 동안 발생하는 중요한 순간들을 포착하여 추가적인 정보를 제공하므로, 이를 수집하고 저장하는 것은 시스템의 행동을 더욱 잘 이해하고 문제를 효과적으로 해결하는 데 중요하다. (에러모니터링, 성능분석, 사용자 행동분석등에 쓰일 수 있음)

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

app = FastAPI()

trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)
trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))

# /를 요청하면 "Hello, World!"를 반환하며, 요청을 받을때와 응답을 보낼때 이벤트에 기록
@app.get("/")
async def hello():
    with tracer.start_as_current_span("hello_request") as span:
        span.add_event("Received request", {"path": "/"})
        response = "Hello, World!"
        span.add_event("Sending response", {"response": response})
        return response

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

[8] 링크

링크(Link) 는 여러 스팬(Span) 사이의 관계를 나타내는 구조다.
스팬간의 관계를 표현하며, 스팬은 다른 스팬에 연결하는 스팬 링크를 생성할 수 있다. 연결은 스팬들이 서로 다른 추적에 속하거나 동일 추적에 속하면서도 복잡한 상호 작용을 가질 때 유용하다.

  • 사용사례
    • 비동기 작업
    • 배치처리
    • 복잡한 의존성

링크를 만들기 위해서는 스팬 콘텍스트가 필요하다.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# 트레이서 프로바이더 초기화
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)
trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))

# 첫 번째 스팬 생성
with tracer.start_as_current_span("first_operation") as first_span:
    # 첫 번째 작업을 수행

    # 두 번째 스팬 생성, 첫 번째 스팬과 링크를 생성하여
    with tracer.start_as_current_span("second_operation", links=[trace.Link(first_span.get_span_context())]) as second_span:
        # 두 번째 작업을 수행
        pass

    # 첫 번째 스팬과는 독립적으로 세 번째 스팬을 생성하되 첫 번째 스팬과 링크
    with tracer.start_as_current_span("third_operation", links=[trace.Link(first_span.get_span_context())]) as third_span:
        # 세 번째 작업을 수행
        pass

6.2.2 Context Propagation (콘텍스트 전파)

- 참고: https://opentelemetry.io/docs/concepts/context-propagation/

컨텍스트 전파(Context Propagation) 는 분산 시스템 내에서 추적(Trace) 데이터나 메타데이터를 서비스 간에 전달하는 메커니즘이다.

컨텍스트 전파를 통해 요청이 다양한 서비스를 거치면서도 연관된 추적 정보(예: 스팬 ID, 추적 ID)와 메타데이터(예: Baggage)를 유지하고 관리할 수 있다.
추적에만 국한되지는 않지만, 추적을 통해 프로세스 및 네트워크 경계에 임의로 분산된 서비스 전반에 걸쳐 시스템에 대한 인과정보를 구축할 수 있다.

콘텍스트 전파에는 두가지 핵심 개념이 있다.

Context

- 참고: Context

컨텍스트는 추적 정보(예: 추적 ID, 스팬 ID)와 메타데이터를 포함하는 객체로, 서비스 간 요청의 연관성을 파악하고 추적하는 데 필요한 정보를 담고있다.

Propagation

전파는 서비스나 프로세스 간에 컨텍스트를 이동시키는 메커니즘을 의미한다.
컨텍스트 객체는 직렬화(serialize)되거나 역직렬화(deserialize)되어, 필요한 추적 정보가 다음 서비스로 전달된다.
일반적으로 이 작업은 OpenTelemetry와 같은 계측 라이브러리에 의해 자동으로 처리되며, 개발자가 직접 관여할 필요는 없다.

기본적으로 W3C의 TraceContext 명세를 따르는 헤더를 사용하여 컨텍스트를 전파한다.

OpenTelemetry는 traceparent, tracestate 헤더를 사용하여 서비스 간에 전파된다.

# traceparent 헤더
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
              [1]                 [2]                   [3]      [4]
  1. version: traceparent 헤더의 버전
  2. TraceID: 전체 추적을 고유하게 식별하는 16바이트 길이의 식별자
  3. Perent Spen ID: 현재 스팬의 부모 스팬을 식별하는 8바이트 길이의 식별자
  4. Trace Flag: 추적과 관련된 추가적인 정보를 제공하는 1바이트 길이의 플래그 (샘플링 여부, "01" 샘플링 활성화)
# tracestate 헤더
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7
            [key]=[value]    ,[key]=[value]

Tracestate 헤더에는 Traceparent 헤더와 함께 추가 식별자를 전파할 수 있는 임의 데이터의 키-값 쌍이 포함된다.

위에서 언급한 Baggage랑 약간 헷갈리는데, 역할과 목적이 다르다.

  • Tracestate:
    • traceparent 헤더와 함께 추적 컨텍스트를 전파하는 데 사용
    • 추적시스템이 필요로하는 기술적, 운영적 메타데이터전파
  • Baggage:
    • 사용자 정의 데이터를 전역적으로 전파하는 데 사용
    • 애플리케이션 수준의 의미있는 비즈니스/운영 데이터 전파

6.2.3 Pipeline (파이프라인)

Pipeline

파이프라인(Pipeline) 은 데이터 수집, 처리 및 전송의 흐름을 설명하는 개념이며, 텔레메트리 신호(추적,메트릭,로그)를 수집, 변환, 수출하는 과정을 포함한다.

이해하기 어려워서 코드에서 어떻게 사용하는지 확인하면 좋을것같다.

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

app = FastAPI()

# Provider: TracerProvider 설정
trace.set_tracer_provider(TracerProvider())

# Exporter: 추적 데이터 콘솔로 출력
exporter = ConsoleSpanExporter()

# Processor: SimpleSpanProcessor를 사용하여 스팬 데이터를 즉시 Exporter에 보냄
processor = SimpleSpanProcessor(exporter)
trace.get_tracer_provider().add_span_processor(processor)

# Telemetry Generator: FastAPI 애플리케이션을 자동으로 추적하도록 설정
FastAPIInstrumentor.instrument_app(app)

# 기본 루트 경로: 간단한 HTTP GET 요청을 처리하고 추적
@app.get("/")
async def read_root():
    tracer = trace.get_tracer(__name__)
    # Telemetry Generator: API 요청을 추적합니다.
    with tracer.start_as_current_span("read_root"):
        return {"Hello": "World"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
  1. Provider (프로바이더)
    • 텔레메트리 파이프라인의 시작점은 Provider다.
    • SDK의 초기 설정 단계에서 트레이서나 미터를 생성하는 데 사용되며, 데이터 수집의 시작점 역할을 한다.
    • TracerProvider를 사용하여 신호 데이터를 관리하고 초기화한다.
    • 추적(TracerProvider), 메트릭(MeterProvider), 로그 (LoggerProvider) 등의 프로바이더가 있다.
  2. Telemetry Generator (텔레메트리 생성기)
    • 실제 애플리케이션 코드에서 데이터를 생성하는 역할을 한다.
    • SDK가 설치된 환경에서 실행되어 사용자의 요청을 추적하거나 메트릭 데이터를 생성한다.
  3. Processor (프로세서)
    • 수집된 데이터를 처리하는 컴포넌트로, 데이터 필터링, 샘플링, 변형 등을 수행할 수 있다.
    • 데이터를 수출하기 전에 필요한 형식으로 조정하는 단계다.
  4. Exporter (익스포터)
    • 처리된 데이터를 최종적으로 외부 시스템(예: 모니터링 도구, 분석 서비스)으로 전송하는 역할을 한다.
    • 다양한 타입의 익스포터가 존재하며, 각각 특정 백엔드 시스템에 최적화되어 있다.

Collector

- 참고링크: https://opentelemetry.io/docs/collector/

Pipeline을 보다보니, 비슷한 개념인 Collector와 약간 혼동되었다.

위에서 설명하고있는 네가지 요소 Provider, Generator, Processer, Exporter는 OpenTelemetry의 텔레메트리 데이터를 처리하는 파이프라인의 구성 요소이며, SDK내에서 구현된다.
즉, Pipeline은 SDK에서 구현되며 애플리케이션 코드 내에 직접 통합되어 데이터의 수집과 처리 수출을 처리한다.

반면에, OpenTelemetry Collector는 이러한 텔레메트리 파이프라인을 물리적으로 분리하여, 독립적인 서비스나 에이전트 형태로 배포할 수 있게 해주는 별도의 구성 요소이다.

Collector는 네트워크를 통해 다양한 소스로부터 텔레메트리 데이터를 수집하고, 이를 처리한 후 다양한 백엔드 시스템으로 전송하는 역할을 한다. (설치도 도커, 쿠버네티스, Linux apk rpm 등등 여러 옵션을 제공한다.(참고))

6.3 추적

6.3.1 오픈텔레메트리 추적 소개

추적(Trace)는 사용자나 애플리케이션이 요청을 할때 어떤일이 발생하는지에 대한 큰 그림을 제공한다.

샘플 Trace 코드

아래 추적 예제를 통해 어떻게 추적하는지 확인해보자.

pip install opentelemetry-sdk opentelemetry-api
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# 트레이서 프로바이더 초기화
tracer_provider = TracerProvider()

# 콘솔 익스포터 설정
exporter = ConsoleSpanExporter()
processor = SimpleSpanProcessor(exporter)
tracer_provider.add_span_processor(processor)

# 전역 트레이서 프로바이더 설정
trace.set_tracer_provider(tracer_provider)

# 트레이서 얻기
tracer = trace.get_tracer(__name__)

# 중첩된 스팬 생성
with tracer.start_as_current_span("Red") as span_red:
    # 'Red' 스팬 시작
    with tracer.start_as_current_span("Green") as span_green:
        # 'Green' 스팬 시작
        with tracer.start_as_current_span("Blue") as span_blue:
            # 'Blue' 스팬 시작
            print("테스트")

이걸 실행시키면 JSON 형식의 출력이 나온다. 각 항목을 확인해보자.

# 출력 결과

테스트
{
    "name": "Blue",
    "context": {
        "trace_id": "0x7ed659dfc0ffd4525411421dd32c5b0d",
        "span_id": "0x892c5d277b377e8b",
        "trace_state": "[]"                               
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x95bfd1111d35c3d1",
    "start_time": "2024-04-14T13:04:30.679814Z",
    "end_time": "2024-04-14T13:04:30.679839Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Green",
    "context": {
        "trace_id": "0x7ed659dfc0ffd4525411421dd32c5b0d",
        "span_id": "0x95bfd1111d35c3d1",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x4b539330e3f8385e",
    "start_time": "2024-04-14T13:04:30.679798Z",
    "end_time": "2024-04-14T13:04:30.680267Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Red",
    "context": {
        "trace_id": "0x7ed659dfc0ffd4525411421dd32c5b0d",
        "span_id": "0x4b539330e3f8385e",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2024-04-14T13:04:30.679770Z",
    "end_time": "2024-04-14T13:04:30.680328Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}

샘플 Trace 코드 + Attributes

Attribute는 스팬에 대한 추가적인 정보를 제공한다. (태그와 비슷)

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# 트레이서 프로바이더 초기화
tracer_provider = TracerProvider()

# 콘솔 익스포터 설정
exporter = ConsoleSpanExporter()
processor = SimpleSpanProcessor(exporter)
tracer_provider.add_span_processor(processor)

# 전역 트레이서 프로바이더 설정
trace.set_tracer_provider(tracer_provider)

# 트레이서 얻기
tracer = trace.get_tracer(__name__)

# 중첩된 스팬 생성
with tracer.start_as_current_span("Red", attributes={"color": "red"}) as span_red:
    # 'Red' 스팬 시작
    with tracer.start_as_current_span("Green", attributes={"color": "green"}) as span_green:
        # 'Green' 스팬 시작
        with tracer.start_as_current_span("Blue", attributes={"color": "blue"}) as span_blue:
            # 'Blue' 스팬 시작
            print("테스트")

결과는 아래와 같다. 추가 정보가 attributes에 추가되었다.

테스트
{
    "name": "Blue",
    "context": {
        "trace_id": "0x02eb6338a1f40daf7230e2df981eabf0",
        "span_id": "0xb7518ad715b93ed0",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x88769b3b31af6642",
    "start_time": "2024-04-14T13:12:18.444421Z",
    "end_time": "2024-04-14T13:12:18.444444Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "blue"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Green",
    "context": {
        "trace_id": "0x02eb6338a1f40daf7230e2df981eabf0",
        "span_id": "0x88769b3b31af6642",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xedc13ebcf868ffe4",
    "start_time": "2024-04-14T13:12:18.444405Z",
    "end_time": "2024-04-14T13:12:18.444880Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "green"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Red",
    "context": {
        "trace_id": "0x02eb6338a1f40daf7230e2df981eabf0",
        "span_id": "0xedc13ebcf868ffe4",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2024-04-14T13:12:18.444377Z",
    "end_time": "2024-04-14T13:12:18.444940Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "red"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}

샘플 Trace 코드 + Attributes + events

이벤트(Event)는 스팬(Span) 내에서 발생하는 주요 순간이나 상태 변화를 나타내는 데 사용한다.
이벤트를 추가하는 것은 추적 데이터에 중요한 타임스탬프와 관련 정보를 포함시켜 분석 시 더 많은 컨텍스트를 제공할 수 있다.

'Blue' 스팬에 이벤트를 추가해보자.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# 트레이서 프로바이더 초기화
tracer_provider = TracerProvider()

# 콘솔 익스포터 설정
exporter = ConsoleSpanExporter()
processor = SimpleSpanProcessor(exporter)
tracer_provider.add_span_processor(processor)

# 전역 트레이서 프로바이더 설정
trace.set_tracer_provider(tracer_provider)

# 트레이서 얻기
tracer = trace.get_tracer(__name__)

# 중첩된 스팬 생성
with tracer.start_as_current_span("Red", attributes={"color": "red"}) as span_red:
    # 'Red' 스팬 시작
    with tracer.start_as_current_span("Green", attributes={"color": "green"}) as span_green:
        # 'Green' 스팬 시작
        with tracer.start_as_current_span("Blue", attributes={"color": "blue"}) as span_blue:
            # 'Blue' 스팬 시작
            # 이벤트 추가
            span_blue.add_event("Important Step Reached", {"step": "initialization"})
            print("테스트")

결과는 아래와 같다.

테스트
{
    "name": "Blue",
    "context": {
        "trace_id": "0xca57089d5d70504a5a2d8298ef153326",
        "span_id": "0x0f923cd0477c968d",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x5ffc31811c7f0d2f",
    "start_time": "2024-04-14T13:18:39.256182Z",
    "end_time": "2024-04-14T13:18:39.256210Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "blue"
    },
    "events": [             # 이벤트 추가됨!!!
        {
            "name": "Important Step Reached",
            "timestamp": "2024-04-14T13:18:39.256188Z",
            "attributes": {
                "step": "initialization"
            }
        }
    ],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Green",
    "context": {
        "trace_id": "0xca57089d5d70504a5a2d8298ef153326",
        "span_id": "0x5ffc31811c7f0d2f",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xdc7e7fd90c4899c3",
    "start_time": "2024-04-14T13:18:39.256164Z",
    "end_time": "2024-04-14T13:18:39.256639Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "green"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Red",
    "context": {
        "trace_id": "0xca57089d5d70504a5a2d8298ef153326",
        "span_id": "0xdc7e7fd90c4899c3",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2024-04-14T13:18:39.256136Z",
    "end_time": "2024-04-14T13:18:39.256700Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "red"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}

샘플 Trace 코드 + Attributes + events + links

링크(Link)는 한 스팬과 다른 스팬이나 트레이스 간의 연결을 나타내는 데 사용한다.
다른 트레이스에 속하는 스팬과의 연관성을 표현하거나, 특정 작업의 연관된 스팬을 참조할 때 유용하다.

'Blue' 스팬에 링크를 추가해보자!

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider, RandomIdGenerator
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.trace import SpanContext, TraceFlags, TraceState

# 트레이서 프로바이더 초기화
tracer_provider = TracerProvider()

# 콘솔 익스포터 설정
exporter = ConsoleSpanExporter()
processor = SimpleSpanProcessor(exporter)
tracer_provider.add_span_processor(processor)

# 전역 트레이서 프로바이더 설정
trace.set_tracer_provider(tracer_provider)

# 트레이서 얻기
tracer = trace.get_tracer(__name__)

# 다른 스팬의 컨텍스트 생성 (예제용)
id_generator = RandomIdGenerator()
trace_id = id_generator.generate_trace_id()
span_id = id_generator.generate_span_id()
other_span_context = SpanContext(
    trace_id=trace_id,
    span_id=span_id,
    is_remote=True,
    trace_flags=TraceFlags.DEFAULT,
    trace_state=TraceState()
)

# 중첩된 스팬 생성
with tracer.start_as_current_span("Red", attributes={"color": "red"}) as span_red:
    # 'Red' 스팬 시작
    with tracer.start_as_current_span("Green", attributes={"color": "green"}) as span_green:
        # 'Green' 스팬 시작
        with tracer.start_as_current_span("Blue", attributes={"color": "blue"}, links=[trace.Link(other_span_context)]) as span_blue:
            # 'Blue' 스팬 시작
            # 이벤트 추가
            span_blue.add_event("Important Step Reached", {"step": "initialization"})
            print("테스트")

이제 blue Spen에 링크가 추가되었다.

테스트
{
    "name": "Blue",
    "context": {
        "trace_id": "0xb86e3902eb3919b10076b02d6f46377b",
        "span_id": "0x15c255b44bfa9ed0",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x2342aca79612d0bd",
    "start_time": "2024-04-14T13:35:29.000877Z",
    "end_time": "2024-04-14T13:35:29.000911Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "blue"
    },
    "events": [
        {
            "name": "Important Step Reached",
            "timestamp": "2024-04-14T13:35:29.000886Z",
            "attributes": {
                "step": "initialization"
            }
        }
    ],
    "links": [     # 링크 생성
        {
            "context": {
                "trace_id": "0xdb58530034b9cd00f08ef767b5199793",
                "span_id": "0x8586c58f9af73b75",
                "trace_state": "[]"
            },
            "attributes": {}
        }
    ],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Green",
    "context": {
        "trace_id": "0xb86e3902eb3919b10076b02d6f46377b",
        "span_id": "0x2342aca79612d0bd",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xd419153e6f98cab8",
    "start_time": "2024-04-14T13:35:29.000857Z",
    "end_time": "2024-04-14T13:35:29.001474Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "green"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "Red",
    "context": {
        "trace_id": "0xb86e3902eb3919b10076b02d6f46377b",
        "span_id": "0xd419153e6f98cab8",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2024-04-14T13:35:29.000822Z",
    "end_time": "2024-04-14T13:35:29.001557Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "color": "red"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}

6.3.2 추적 파이프라인 구성

이 부분은 도저히.. 뭘 설명하고자 하는건지, 어떻게 테스트하라는건지 알수가 없어 일단 그냥 넘어간다..

6.4 메트릭

6.4.1 오픈텔레메트리 메트릭 소개

메트릭은 애클리케이션에서 일어나는 일에 대한 정보를 이해할 수 있는 정보를 제공한다.

오픈텔레메트리 메트릭은 카운터, 게이지, 히스토그램 등 다양한 메트릭 타입을 지원한다.
기존에 사용하던 프로메테우스 메트릭 API와는 많은 차이점이 있다.
프로메테우스는 4개의 메트릭 유형을 제공하지만, 오픈텔레메트리는 6가지 메트릭 유형을 제공한다.

샘플 Metric 코드

pip install opentelemetry-api opentelemetry-sdk
# https://github.com/open-telemetry/opentelemetry-python/blob/main/docs/examples/metrics/reader/preferred_aggregation.py
import time

from opentelemetry.metrics import get_meter_provider, set_meter_provider
from opentelemetry.sdk.metrics import Counter, MeterProvider
from opentelemetry.sdk.metrics.export import (
    ConsoleMetricExporter,
    PeriodicExportingMetricReader,
)
from opentelemetry.sdk.metrics.view import LastValueAggregation

aggregation_last_value = {Counter: LastValueAggregation()}

exporter = ConsoleMetricExporter(
    preferred_aggregation=aggregation_last_value,
)

reader = PeriodicExportingMetricReader(
    exporter,
    export_interval_millis=5_000,
)

provider = MeterProvider(metric_readers=[reader])
set_meter_provider(provider)

meter = get_meter_provider().get_meter("preferred-aggregation", "0.1.2")

counter = meter.create_counter("my-counter")

for x in range(10):
    counter.add(x)
    time.sleep(2.0)

이 코드를 실행하면 결과는 아래와 같다.

import time

from opentelemetry.metrics import get_meter_provider, set_meter_provider
from opentelemetry.sdk.metrics import Counter, MeterProvider
from opentelemetry.sdk.metrics.export import (
    ConsoleMetricExporter,
    PeriodicExportingMetricReader,
)
from opentelemetry.sdk.metrics.view import LastValueAggregation

# 특정 메트릭 인스트루먼트 유형(Counter 등)에 대해 사용할 기본 집계 방식을 정의 (어떻게 집계할 것인지)
aggregation_last_value = {Counter: LastValueAggregation()}

# 메트릭 익스포터 초기화
# 콘솔에 메트릭 데이터를 내보내는 역할을 수행한다.
exporter = ConsoleMetricExporter(
    preferred_aggregation=aggregation_last_value,
)

# 메트릭 리더 초기화
# 설정된 간격(여기서는 5000 밀리초, 즉 5초마다)으로 메트릭을 수집하고 익스포터를 통해 내보내는 역할을 수행한다.
reader = PeriodicExportingMetricReader(
    exporter,
    export_interval_millis=5_000,
)

# 메트릭 프로바이더 초기화
# 메트릭 리더를 등록하여 수집된 메트릭 데이터를 처리하고 내보내는 역할을 수행한다
provider = MeterProvider(metric_readers=[reader])
set_meter_provider(provider)

# get_meter 함수를 사용해 메트릭을 생성하고 업데이트하는 데 필요한 Meter 인스턴스를 생성한다.
# Meter는 메트릭의 측정(즉, 데이터 포인트 생성)을 책임지는 객체이다.
meter = get_meter_provider().get_meter("preferred-aggregation", "0.1.2")

counter = meter.create_counter("my-counter")

for x in range(10):
    counter.add(x)
    time.sleep(2.0)

어휴 그런데, 생각보다 metric이 복잡하고 어렵다.
더 들어가기전에 용어와 구성요소를 확인하고 가는게 좋을 것 같다.

OpenTelemetry 메트릭 용어 정리

  [MetricProvider] # 메트릭 시스템의 시작점으로, 메트릭 데이터 수집을 조정하고 Meter 인스턴스를 제공한다.
         ↓
      [Meter] # 메트릭 인스트루먼트를 생성하고 관리한다. (애플리케이션 코드에서 메트릭을 정의할 때 사용)
         ↓
   [Instrument] # 개별 측정 작업을 실행하는 객체 (카운터, 게이지 등)
         ↓
   [Measurement] # 실제 측정된 값을 나타내는 데이터 포인트, 인스트루먼트에서 메트릭 값을 측정할 때 생성됨
         ↓
  [MetricReader] # 수집된 메트릭을 처리하고 준비하여 메트릭 익스포터로 전달
         ↓
  [MetricExporter] # 처리된 메트릭 데이터를 외부 시스템으로 전송
         ↓
 [External System]
  • Meter:
    • 메트릭 데이터(측정값)를 생성하는 인터페이스를 제공한다.
    • 다양한 유형의 메트릭 Instrument를 생성한다.
  • Instrument:
    • 측정을 수행하는 객체 (예 Counter, ObservableGauge 등)
    • 애플리케이션의 특정 부분에서 측정을 수집하는 데 사용된다.
  • Measurement:
    • 실제로 측정된 데이터 포인트를 나타낸다.
    • 메트릭 인스트루먼트가 메트릭 값을 수집할 때마다 측정이 생성된다.
  • MetricReader:
    • 수집된 메트릭을 처리하고 최종적으로 메트릭 익스포터로 전달한다.
    • 메트릭을 주기적으로 수집하고, 필요에 따라 즉시 내보내거나 종료 처리를 수행할 수 있다.

6.4.2 메트릭 파이프라인 구성

이 부분도..... 내일의 나에게 부탁한다..

6.5. 로그

책에서 다루는 내용은 공식문서에서 거의 찾아볼 수없어, 별도로 아래 공식Docs를 통해 내용을 정리했다 (책내용 🙅🏻‍♀️아님🙅🏻‍♀️)

* 참고링크: https://opentelemetry.io/docs/specs/otel/logs/

메트릭(metrics)과 트레이스(traces)에 대해서는 새로운 API를 지정하고, 여러 언어에서 이 API의 전체 구현을 제공하는 '청사진 설계(clean-sheet design)' 접근 방식을 취하고 있다.

하지만 로그는 기본적으로 프로그래밍 언어에 내장된 로깅 기능이 있고, 널리 사용하는 로깅 라이브러리가 있기때문에, Metric, Trace와 다르게 기존 기술과의 호환성을 유지하면서, 기존 로그 메세지 형식에 추적 등의 상관관계 정보와 텔레메트리 정보를 추가하도록 개발되었다.

로그에 대한 OpenTelemetry의 접근방식

  • 로그데이터 모델 정의 (참고): 로그 데이터 모델을 정의하여 LogRecord가 무엇인지, 어떤 데이터가 기록, 전송, 저장 및 해석되어야 하는지에 대한 규칙을 정의했다.
  • 로그 브릿지 API(참고): LogRecords를 발행하기 위한 Logs Bridge API를 정의했다. 애플리케이션 개발자는 이 API를 직접 호출하는 것이 권장되지 않으며, 이는 기존 로깅 라이브러리와 OpenTelemetry 로그 데이터 모델 간의 다리 역할을 하는 로그 어펜더를 구축하기 위해 라이브러리 저자들에게 제공된다.
  • 브리지 API의 SDK 구현(참고): 브리지 API의 SDK 구현을 정의하여 LogRecords의 처리 및 내보내기를 구성할 수 있다.
  • 새로운 로그 시스템(참고): 새롭게 설계된 로그 시스템은 OpenTelemetry의 로그 데이터 모델에 따라 로그를 생성한다.
  • 기존 로그 포맷 호환성: 기존 로그 포맷은 OpenTelemetry 로그 데이터 모델로 명확하게 매핑할 수 있다. OpenTelemetry Collector를 통해 OpenTelemetry 로그 데이터 모델로 변환할 수있다.

OpenTelemetry 로그 소스 유형

로그 소스 유형을 나누고 정의함으로써, 로그의 수집 및 처리 방식에 있어서 구체적인 접근 방식을 제시하고 있다.

  • System Logs (시스템 로그):
    • 운영 체제가 생성하는 로그로, 로그의 형식이나 포함되는 정보를 변경할 수 없다. (syslog, windoes event logs..)
    • OpenTelemetry Collector를 통해 시스템 로그는 자동으로 리소스 컨텍스트(예: 호스트 이름, IP 주소)를 추가하고 수집할 수 있다.
  • Infrastructure Logs (인프라 로그):
    • 인프라 로그는 네트워크 장비, 데이터 센터, 클라우드 서비스, Kubernetes 이벤트 와 같은 IT 다양한 인프라 구성 요소에서 생성되는 로그를 말한다.
    • OpenTelemetry Collector를 통해 노드, 포트, 컨테이너 등의 리소스 컨텍스트를 추가하고 수집할 수 있다.
  • Third-party Application Logs (제3자 애플리케이션 로그):
    • 서드파티 애플리케이션 로그는 잘 알려진 어플리케이션, 예를들어 mysql과 같은 솔루션을 사용했을때의 로그를 말한다.
    • OpenTelemetry는 OpenTelemetry Collector의 filelog receiver 사용하거나, FluentBit과 같은 로그 수집 에이전트를 사용하여 다시 OpenTelemetry Collector로 전송하여 로그를 추가로 처리하는것을 권장한다.
  • Legacy First-Party Applications Logs (레거시 첫 번째 파티 애플리케이션 로그):
    • 사내에서 개발된 애플리케이션에서 발생하는 로그를 말한다.
    • OpenTelemetry는 로그를 수집할때 일반 텍스트가 아닌 json과 같은 구조화된 형식으로 출력하도록 어플레케이션의 로그 포멧터를 재구성하고 (로그수집 신뢰성 향상), 로그마다 TraceID, SpanID를 추가하도록 권장한다.
    • 일부 로깅 라이브러리는 log appender 혹은 log bridge를 사용할 수 있으므로, 이를 사용하여 로그 레코드에 추가정보(TraceID, SpenID)를 추가하도록 권장한다.
  • Via File or Stdout Logs (파일이나 표준 출력을 통한 로그):
    • 로그가 파일 또는 표준 출력으로 작성되는 경우를 말한다.
    • OpenTelemetry는 OpenTelemetry Collector의 filelog receiver 사용하거나, FluentBit과 같은 로그 수집 에이전트를 사용하여 다시 OpenTelemetry Collector로 전송하여 로그를 추가로 처리하는것을 권장한다.
  • Direct to Collector (직접 컬렉터로 전송):
    • 로그를 OpenTelemetry Protocol을 통해 출력되도록 직접 어플리케이션을 수정하여 사용하는것을 말한다.
    • 장점: 로그 데이터를 파일이나 다른 중간 저장소를 거치지 않고, 직접 OpenTelemetry Collector 같은 데이터 수집 시스템으로 전송하는 방법을 이 방식은 데이터 처리의 지연을 최소화하고 효율을 높일 수 있다.
    • 단점: 로컬 파일로 로그를 보유하는 간단함이 사라지며, 로그를 수신할 목적지가 로그를 받을 수 있을때만 작동한다.
    • 로컬파일에도 로그를 적재하고, OTLP로 컬렉터로 직접 전송 가능하도록 하기 위해 Bridge APISDK를 제공한다.
  • New First-Party Application Logs (새 첫 번째 파티 애플리케이션 로그):
    • 새로 개발되고있는 어플리케이션에서 어떻게 로그를 보내는것이 좋은지에 대한 내용이다.
    • 로그 라이브러리를 기존처럼 사용하고, Bridge APISDK를 사용하여 로그에 TraceID 등을 추가한다.

https://opentelemetry.io/docs/specs/otel/logs/#legacy-and-modern-log-sources

Log 코드 테스트 해보기

샘플 FastAPI 코드

main.py를 생성하고 아래 코드를 복사한다.

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}

아래 명령어를 실행한다.

$ pip install fastapi uvicorn
$ uvicorn main:app --reload

어플리케이션이 실행되는지 http://127.0.0.1:8000에 접속해서 확인해보자

샘플 FastAPI 코드 + Trace추가

위 코드에 Trace를 추가해보자.

아까 생성한 main.py에 내용을 삭제하고 아래 코드를 복사하여 붙여넣자.

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

app = FastAPI()

# OpenTelemetry 트레이서 설정
trace.set_tracer_provider(
    TracerProvider(
        resource=Resource.create({SERVICE_NAME: "simple-fastapi-service"})
    )
)
tracer = trace.get_tracer(__name__)

# OTLP로 트레이스 데이터를 localhost로 전송
otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# FastAPI 앱에 OpenTelemetry 구성
FastAPIInstrumentor.instrument_app(app)

@app.get("/")
def read_root():
    with tracer.start_as_current_span("root_span"):
        return {"Hello": "World"}

아래 명령어를 실행한다.

$ pip install fastapi uvicorn opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi opentelemetry-exporter-otlp
$ uvicorn main:app --reload

어플리케이션이 실행되는지 http://127.0.0.1:8000에 접속해서 확인해보자

백엔드를 설정해두었다면 trace가 정상적으로 찍혀 보일것이다.

샘플 FastAPI 코드 + Trace + Log추가

이제 위 코드에 로그를 추가해보자.

아까 생성한 main.py에 내용을 삭제하고 아래 코드를 복사하여 붙여넣자.

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
import logging

app = FastAPI()

# OpenTelemetry 트레이서 설정
trace.set_tracer_provider(
    TracerProvider(
        resource=Resource.create({SERVICE_NAME: "simple-fastapi-service"})
    )
)
tracer = trace.get_tracer(__name__)

# OTLP로 트레이스 데이터를 localhost로 전송
otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# FastAPI 앱에 OpenTelemetry 구성
FastAPIInstrumentor.instrument_app(app)

# 로깅 구성
LoggingInstrumentor().instrument(set_logging_format=True)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

@app.get("/")
def read_root():
    with tracer.start_as_current_span("root_span"):
        logging.info("Handling root request")
        return {"Hello": "World"}

아래 명령어를 실행한다.

$ pip install opentelemetry-instrumentation-logging
$ uvicorn main:app --reload

어플리케이션이 실행되는지 http://127.0.0.1:8000에 접속해서 확인해보자

어플리케이션 실행 후 웹브라우저에서 http://127.0.0.1:8000 접속하면 TraceID 터미널에 출력됨 확인
백엔드(tempo)에서 TraceID가 검색되는지 확인

설정한대로 로그에 나온 TraceID를 확인할수 있었다.

6.6 컬렉터

오픈텔레메트리 컬렉터(OpenTelemetry Collector) 개요

컬렉터는 다양한 소스(애플리케이션, 시스템, 인프라 등)에서 로그, 메트릭, 트레이스 데이터를 수집, 변환 하고 처리한 후 여러 백엔드(예: 모니터링 도구, 로그 분석 서비스)로 전송한다.

OpenTelemetry를 테스트하거나 소규모 환경에서는 Collector없이 바로 백엔드로 보낼 수 있지만,
일반적으로는 Collector를 사용하는것을 권장한다.
Collector에서 재시도, 일괄처리, 암호화, 민감데이터 필터링 등 추가 데이터 처리가 가능하기 때문이다.

설치 방법

설치 방법도 간단하다.
docker 컨테이너로 실행하거나, 바이너리로 실행할 수 있다. 참고

Collector 적용 패턴

  1. No Collector (링크)
    • 어플리케이션에서 바로 백엔드로 보내는 구성을 말한다.
    • 위에 나왔던 샘플코드들은 Metric, Log, Trace들을 OTLP를 이용해서 바로 백엔드로 보냈다.
    • No Collector 패턴은 사용하기 간편하고, 추가 설정할 필요가 없다는 장점이 있지만, 수집, 처리,내보내기 엔드포인트 등 수정이 필요한경우 코드에 수정이 필요하다.
    • 또한 내보내기 수가 제한되어있어 제한 사항을 확인해야하며, 재시도 등에 대해 직접 고민해야한다.
    • 일반적으로 No Collector 구성은 테스트환경이거나 아주 소규모일때만 사용하도록 권장한다.
  2. Agent (링크)
    • 서버당 하나의 Collector를 구성하거나, 사이드카 컨테이너로 구성하는 방식을 말한다.
    • 구성이 간단하다는 장점이 있으나, 확장성 문제(서버수가 증가할수록 컬렉터도 많아짐)가 발생할 수 있고, 유연성이 부족하다.
  3. Gateway (링크)
    • Collector 서버를 따로 구성하는 방법을 말한다. (Collector 서버구성)
    • 중앙집중화된 컬렉터 서버그룹을 사용하는 방식으로, 데이터 처리 및 관리를 중앙에서 수행할 수 있다.
    • 장점으로는 보안정책이나 설정 등을 중앙에서 관리할 수 있으며, 개별 어플리케이션 설정에 대해 신경쓸 필요가 없다.
    • 단점으로는 시스템의 복잡성을 증가시키고, 추가적인 유지보수가 필요하다. 또한,  중앙 컬렉터가 실패할 경우 시스템 전체의 데이터 수집에 영향을 줄 수 있으므로 단일실패지점이 될 수 있다.
    • 또한 데이터가 여러단계에 걸쳐 수집되기때문에 처리 지연시간이 발생할 수 있도, 상당한 양의 컴퓨팅 리소스가 추가되어 운영비용의 증가로 이어질 수 있다.

Collector 구성요소

Receivers (링크)

  • 데이터가 Collector로 들어가는 방법을 정의한다.
  • Receiver는 지정된 형식의 데이터를 받아 내부 형식으로 변환한 후 해당 파이프라인에 정의된 Processer 와 Exporter에 전달한다.
  • Collector에서는 하나이상의 Receiver가 필요하다.
  • Collector는 구성하더라도 바로 활성화 되지 않는다. Service 섹션 내의 적절한 파이프라인에 추가해야 활성화 된다.
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

Processors (링크)

  • Processer는 Receivers에서 수집된 데이터를 가져와 수정하거나 변환한 후 Exporter에 전달한다.
  • 필터링, 삭제, 이름바꾸기, 재계산, 메타데이터 추가, 샘플링 등의 작업이 포함되며 파이프라인의 순서에 따라 처리하는 작업의 순서가 결정된다.
  • Processer를 구성하더라도 바로 활성화 되지 않는다. Service 섹션 내의 적절한 파이프라인에 추가해야 활성화 된다.
processors:
  attributes:
    actions:
      - key: environment
        value: "production"
        action: insert
      - key: service_name
        value: "example-service"
        action: insert

Exporters (링크)

  • Exporter는 하나이상의 백엔드로 데이터를 내보낸다.
  • Exporter는 pull 또는 Push 기반일 수 있으며, 하나 이상의 데이터 소스를 지원할 수 있다.
  • Exporter는 구성하더라도 바로 활성화 되지 않는다. Service 섹션 내의 적절한 파이프라인에 추가해야 활성화 된다.
exporters:
  loki:
    endpoint: "http://loki:3100"
  otlp:
    endpoint: "http://tempo:4317"
    protocol: grpc
  prometheusremotewrite:
    endpoint: "http://mimir:9009/api/prom/push"

 

Service

  • Service 섹션에서는 Receiver,Processer,Exporter를 하나의 파이프라인으로 연결한다.
  • 로그, 메트릭, 트레이스 각각에 대해 별도의 파이프라인을 구성한다.
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes]
      exporters: [otlp]
    metrics:
      receivers: [otlp]
      processors: [attributes]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp]
      processors: [attributes]
      exporters: [loki]

Connectors (링크)

  • Connector는 특정 유형의 데이터 처리와 흐름 제어를 담당하는 구성 요소이다.
  • 두 파이프라인을 연결하는 역할을 하며, 한 파이프라인의 끝에서 데이터 Export 역할과 다른 파이프라인의 시작에서 데이터 Reciver 역할을 겸한다.
  • 데이터의 유형이 동일하거나 다를 수 있는 여러 데이터 스트림을 효율적으로 관리하고 연결할 수 있다.
  •  
# count 커넥터가 사용되어 트레이스 데이터를 수집하고, 
#해당 데이터를 메트릭 데이터로 변환하여 다른 파이프라인으로 전송

receivers:
  foo:

exporters:
  bar:

connectors:
  count:
    spanevents:
      my.prod.event.count:
        description: The number of span events from my prod environment.
        conditions:
          - 'attributes["env"] == "prod"'
          - 'name == "prodevent"'

service:
  pipelines:
    traces:
      receivers: [foo]
      exporters: [count]
    metrics:
      receivers: [count]
      exporters: [bar]

6.7 오픈텔레메트리 데모

728x90