본문 바로가기
Coding/DevOps

Datadog Span, Tag 관련 이슈

by Hide­ 2022. 12. 26.
반응형

개요

데이터독에는 일종의 작업 단위를 나타내는 Span이라는 개념이 존재한다. 그리고 Span에 사용자가 원하는 정보를 Tag라는 개념으로 붙여서 표시해줄 수 있다. 본 포스팅에서는 파이썬 httpx 클라이언트를 사용하는 span에 custom tag를 붙여주는 도중 발생한 이슈에 대해 설명하고 해결 방법에 대해 기술한다.

Span Tag

외부와 통신을 할 때 로깅하는 내용을 생각해보면 기본적으로 Header, Request Body, Response Body, Response Status code, Query String 등이 있을 것 같다. ddtrace 라이브러리에서는 Header, Response Status Code, Query String은 아래와 같은 방법으로 Span의 Tag를 통해 추가해줄 수 있다.

Header

from ddtrace import config


config.trace_headers(
    [
        "Host",
        "Content-Type",
        "User-Agent",
        "Authorization",
    ],
)

Query String

from ddtrace import config


config.http.trace_query_string = True

Response Status Code

기본적으로 적용

위에 명시한 내용을 제외한 Request/Response Body의 경우 기본적으로 적용되지 않으므로 사용자가 Custom tag를 추가해줘야한다. 아래와 같은 형태로 작업할 수 있다.

span = tracer.current_span()
if not span:
    return

span.set_tag("http.response.body", "Response")
span.set_tag("http.request.body", "Request")

참고로 str인 경우 set_tag_str()을 사용하면 성능적 이점을 가져갈 수 있다. (https://github.com/DataDog/dd-trace-py/releases/tag/v1.6.0

이슈

예를 들어 아래와 같은 코드가 있다고 가정해보자.

from ddtrace import tracer
from httpx import AsyncClient


span = tracer.current_span()


async with AsyncClient() as client:
    request_body = {"id": 1}
    if span:
        span.set_tag("http.request", request_body)
        
    res = await client.post(url=url, json=request_body)
    
    if span:
        span.set_tag("http.response.body", res.text)

위와 같이 작업을 하고 데이터독의 APM을 통해 살펴보면 

사진처럼 http.request span이 2개가 생성되는 모습을 확인할 수 있다. 그리고 set_tag()를 통해 추가한 tag들은 첫 번째 http.request span에 기록된다. 기본적으로 span에 접근하는 방법은 아래와 같이 총 2가지의 방법이 존재한다.

from ddtrace import tracer


tracer.current_span()
tracer.current_root_span()

current_span()의 경우 현재 작업 단위의 span을 가져오고 current_root_span()의 경우 현재 작업 단위에서 최상위 span을 가져온다. 이렇게 2가지 메소드를 제외하고는 span에 접근하는 다른 메소드가 존재하지 않는데, 이는 기본적으로 현재 span, 최상위 span을 제외하고 이미 지나갔거나 아직 시작하지않은 span에는 접근할 수 없기 때문이다. 첫 번째 코드를 살펴보면 await client.post() 가 호출되기전에 현재의 span에 접근했는데 이때의 span과 실제 post()가 호출될때의 span이 다르기 때문에 위처럼 2개로 나뉘어져서 표시된 것이다.

해결 방법

Custom Filter 

공식문서를 살펴보면 위처럼 Custom하게 Filter를 작성할 수 있다고 나온다. 그런데 process_trace() 메소드의 파라미터인 trace가 List[Span] 이기때문에 반복문을 통해 내가 현재 접근하려는 span이 맞는지 확인해야하는 작업이 필요하다. 

실제로 ddtrace에서 구현되어있는 FilterRequestOnUrl 클래스도 반복문을 사용하는 모습을 확인할 수 있다. 따라서 O(N)의 복잡도가 나오게 된다. 퍼포먼스 하락에 큰 영향이 있을 것 같진 않지만 뭔가 꺼려지는게 사실이다.

Event hook

많은 라이브러리들에서 제공해주는 기능으로 Httpx 또한 동일한 기능을 제공한다. 공식 홈페이지(https://www.python-httpx.org/advanced/#event-hooks) 문서의 코드를 먼저 살펴보자.

def log_request(request):
    print(f"Request event hook: {request.method} {request.url} - Waiting for response")

def log_response(response):
    request = response.request
    print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")

client = httpx.Client(event_hooks={'request': [log_request], 'response': [log_response]})

Request와 Response에 대한 event hook을 제공해준다. 위 작업은 httpx를 사용하여 실제 HTTP Call을 날릴 때 시작/종료와 함께 실행되는 훅이므로 httpx클라이언트가 동작할 때 생성되는 span과 동일한 span에 접근할 수 있다. 참고로 httpx response hook의 경우 훅이 실행되는 당시에는 응답을 읽어오기 전이므로

await response.aread()

코드를 통해 데이터를 읽어온 이후 response에 접근해야한다. 위 방법을 통해 tag를 추가해주면 하나의 span에 원하는 tag가 정상적으로 추가됨을 확인할 수 있다.