OpenTelemetry Python Asyncio 지원

2024. 3. 30. 01:01 개발 이야기/python

 

작년 한국에서 열린 PyCon KR 2023에 참여했을 때, “Improving Debuggability of Complex Asyncio Applications” 발표에 참관했었습니다.

이때 python 표준 라이브러리 중 하나인, asyncio를 사용하면서 문제가 발생했을 때, 애플리케이션 내부에서 무슨 일이 일어나는지 들여다보는 것이 어렵고, "코루틴 작업을 디버깅하기 어렵다"는 모니터링 관점에서의 어려움이 있다는 것을 알게되었습니다.

이런 어려움을 해결하기 위해 aiomonitor 이라는 훌륭한 오픈소스를 개발해서 어려움을 겪는 유저들에게 관측 가능성을 제공한다고 이해했습니다.

제가 생각하기에 aiomonitor 에서 다루는 데이터가 분산 추적 기술에서 많이 사용되는 Trace 데이터와 유사하다고 생각했고,

발표가 끝나고 나서, 발표자님께 분산 추적 오픈소스 인 OpenTelemetry에 기여하실 생각이 없으신이 여쭤봤습니다.

정확히 어느 나라인지는 기억이 나지 않지만, 다른 나라에서 진행한 PyCon 에서 발표했을 때, 같은 질문을 받았다고 하셨습니다. 그리고 기여는 언제나 환영한다는 말씀과 함께요.

이후, 해당 아이디어를 구현하기 위해 작년 11월 경부터 Asyncio의 관측 가능성을 위해 OpenTelemetry-Python-Contrib 프로젝트에 기여 했습니다.

0.45b0 버전부터는 Asyncio를 OpenTelemetry와 함께 모니터링할 수 있게되었습니다.

OpenTelemetry Python을 이용해서 Asyncio coroutine을 추적하는 간단한 예제를 소개하겠습니다.

OpenTelemetry Collector와 Jaeger 실행 Docker-compose과 config
# otel-collector-jaeger.yaml
version: "2"
services:

  # Jaeger
  jaeger-all-in-one:
    image: jaegertracing/all-in-one:latest
    restart: always
    ports:
      - "16686:16686"
      - "14268"
      - "14250"

  # Collector
  otel-collector:
    image: idock.daumkakao.io/matrix/tracing-otel-collector:latest
    restart: always
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"   # OTLP gRPC receiver
      - "4318:4318"
    depends_on:
      - jaeger-all-in-one
# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

exporters:
  otlp:
    endpoint: jaeger-all-in-one:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [ otlp ]
      processors: [ ]
      exporters: [ otlp ]
asyncio coroutine을 실행하는 fastapi 테스트 앱
import asyncio
from fastapi import FastAPI

app = FastAPI()

async def cancellable_coroutine():
    await asyncio.sleep(2)

async def cancellation_coro():
    task = asyncio.create_task(cancellable_coroutine())
    await asyncio.sleep(0.1)
    task.cancel()
    await task

async def cancellation_create_task():
    await asyncio.create_task(cancellation_coro())

@app.get("/hello/")
def say_hello():
    asyncio.run(cancellation_create_task())
    return {"message": "Hello, World!"}
OpenTelemetry 설치 및 앱 실행
$ pip install opentelemetry-api
$ pip install opentelemetry-sdk
$ pip install opentelemetry-instrumentation
$ pip install opentelemetry-instrumentation-asyncio
$ pip install opentelemetry-instrumentation-fastapi
$
$ export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
$ export OTEL_SERVICE_NAME=fastapiSqlalchemy
$ export OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE=cancellation_create_task,cancellable_coroutine,read_notes
$ opentelemetry-instrument --traces_exporter console,otlp --exporter_otlp_endpoint http://localhost:4318 --metrics_exporter none gunicorn main:app --bind 0.0.0.0:8000 

기본적으로 Asyncio의 Instrumentation은 추적하고자하는 coroutine을 OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE 환경변수를 통해 입력해줘야 추적하게 됩니다. 모든 coroutine을 추적하는 것을 기본값으로 지정할 수도 있었지만, 너무 많은 Span을 생성하는 것이 우려되어 이와같이 개발되었습니다.

데모 앱 실행 후 Jaeger UI 확인

이제는 Asyncio의 성능 검증이나, 디버깅, 오류 확인을 OpenTelemetry를 이용해서 하실 수 있습니다. 즐거운 개발, 스트레스 없는 개발 생활 되시길 기원하겠습니다.

추가로, Asyncio 기여 하면서 함께 기여한 Flask 3.0 이상 버전 추적도 0.45b0 버전부터 사용 가능합니다.