파이썬 OpenTelemetry로 PostgreSQL 트레이스와 HTTP 헤더 전파 설정하는 방법
💻 파이썬 OTel 트레이스와 HTTP 클라이언트 헤더 전파 레시피 정리
서비스 로그만 쌓아 두고 어디서 느려지는지 찾지 못해 답답했던 경험, 한 번쯤 있을 거예요.
요즘은 마이크로서비스, 외부 API, 데이터베이스까지 얽혀 있다 보니 한 번의 요청이 어디를 어떻게 거치는지 눈으로 따라가기가 쉽지 않습니다.
그래서 많은 팀이 OpenTelemetry 같은 표준 기반 트레이싱으로 갈아타고 있지만, 실제 코드에 어떻게 녹여야 할지에서 막히는 경우가 많죠.
이 글에서는 그런 지점을 찝어 주는 실전 위주 파이썬 레시피를 함께 살펴보려고 합니다.
핵심은 크게 두 가지입니다.
하나는 데이터베이스 호출을 명확하게 구분해서 추적하기 위한 tracer.start_as_current_span(“db”, attributes={“db.system”:”postgres”}) 패턴이고, 다른 하나는 마이크로서비스 간 연쇄 호출을 한 눈에 이어서 보게 해 주는 HTTP 클라이언트 헤더 전파 설정입니다.
파이썬 애플리케이션에서 OTel 트레이서를 초기화하고, PostgreSQL 쿼리 구간에 span을 심고, requests나 httpx 같은 HTTP 클라이언트로 나가는 요청에 트레이스 컨텍스트를 자연스럽게 실어 보내는 방법까지 차근차근 정리해 보겠습니다.
실제 운영 환경에 바로 옮겨 적을 수 있을 만큼 구체적인 코드 흐름과 함께 정리할 예정이라, 관측성 초보라도 천천히 따라오면 기본 골격을 갖춘 트레이스를 만들 수 있을 거예요.
📋 목차
💻 파이썬 OpenTelemetry 트레이스 기본 개념 정리
OpenTelemetry를 처음 접하면 용어부터 살짝 낯설게 느껴지곤 합니다.
하지만 구조만 한 번 잡아 두면, 파이썬 코드에 필요한 부분만 골라 넣는 레시피처럼 활용할 수 있어요.
OTel 트레이스의 핵심은 요청 하나의 흐름을 트리 형태로 기록하는 것입니다.
가장 바깥쪽에는 전체 요청을 나타내는 루트 span이 있고, 그 안에 데이터베이스 호출, 외부 API 호출, 내부 함수 실행 같은 자잘한 작업들이 자식 span으로 매달리는 구조라고 보면 이해가 쉽습니다.
파이썬에서 이 구조를 만드는 주인공이 바로 tracer 객체입니다.
일반적으로 애플리케이션 시작 시 TracerProvider와 SpanProcessor, Exporter(예: OTLP, Jaeger, Zipkin 등)를 설정하고, 그 위에 get_tracer로 tracer를 하나 얻어 둡니다.
그 다음 애플리케이션의 중요한 구간마다 tracer.start_as_current_span() 같은 컨텍스트 매니저로 span을 열고 닫으면서, 각 구간이 얼마나 걸렸는지, 어떤 속성을 달고 있는지 기록하게 됩니다.
이때 에러 발생 여부나 상태 코드, 쿼리 타입 같은 메타데이터를 attributes로 달아 두면 나중에 대시보드에서 필터링하거나 검색할 때 상당히 도움이 됩니다.
예를 들어 데이터베이스 호출을 기록할 때 자주 쓰는 패턴이 tracer.start_as_current_span(“db”, attributes={“db.system”:”postgres”}) 형태입니다.
여기서 span 이름 “db”는 트레이스 뷰에서 한눈에 이 구간이 데이터베이스 관련 작업이란 걸 알아보기 위한 레이블이고, 속성 db.system=”postgres”는 이 span이 OpenTelemetry 스펙에서 권장하는 PostgreSQL 타입의 데이터베이스 호출임을 명시해 줍니다.
덕분에 나중에 뷰어에서 db.system이 postgres인 span만 모아서 슬로우 쿼리를 찾거나, 특정 서비스의 DB 호출 패턴만 따로 분석하기가 쉬워집니다.
트레이스가 서비스 한 대에서만 끝나면 아쉬운 점이 많습니다.
실제 운영 환경에서는 웹 서버가 HTTP 요청을 받아 내부에서 PostgreSQL을 찍고, 또 다른 마이크로서비스로 HTTP 요청을 던지는 식으로 흐름이 이어지죠.
이때 필요한 개념이 HTTP 클라이언트 헤더 전파입니다.
현재 실행 중인 span의 컨텍스트를 traceparent, tracestate 같은 헤더로 함께 보내 두면, 다음 서비스에서도 그 정보를 이어 받아 자기 쪽 span을 같은 트레이스 안에 연결할 수 있습니다.
이렇게 해야 하나의 사용자 요청이 여러 서비스를 넘나들어도 단일 트레이스 ID로 전체 호출 체인을 재구성할 수 있습니다.
정리하면, 파이썬 OTel 트레이스 설정은 크게 세 단계로 볼 수 있습니다.
(1) Tracer와 Exporter 초기화, (2) tracer.start_as_current_span으로 DB·비즈니스 로직 구간 감싸기, (3) HTTP 클라이언트에서 헤더를 전파해 서비스 간 트레이스를 하나로 묶기.
이 글 전체에서는 이 중에서도 특히 tracer.start_as_current_span(“db”, attributes={“db.system”:”postgres”})로 PostgreSQL 호출을 어떻게 잘 남길지, 그리고 HTTP 클라이언트 헤더 전파를 어떤 식으로 구현하면 실무에서 덜 헷갈리는지에 초점을 맞춰 차근차근 레시피를 이어가 보겠습니다.
🗄️ tracer.start_as_current_span으로 DB 구간 감싸기
데이터베이스 호출은 대부분의 서비스에서 병목이 가장 자주 발생하는 지점입니다.
그래서 트레이스를 구성할 때, DB 호출 앞뒤로 span을 정확히 감싸 두는 것이 전체 성능을 해석하는 데 핵심이 됩니다.
파이썬 OpenTelemetry가 제공하는 tracer.start_as_current_span()은 이때 가장 널리 사용하는 패턴이에요.
컨텍스트 매니저 문법으로 자연스럽게 코드 흐름에 녹아들기 때문에 이미 작성된 DB 호출 코드에도 비교적 쉽게 끼워 넣을 수 있습니다.
예를 들면 다음과 같은 구조입니다.
아래 예시는 PostgreSQL 연결 라이브러리(psycopg, asyncpg 등 무엇이든 가능)에 특별히 의존하지 않는 범용 형태이고, 핵심은 “db”라는 span 이름과 db.system=”postgres” 속성입니다.
이 속성은 OTel 스펙에 따라 PostgreSQL 호출을 명시적으로 태깅해 주는 역할을 합니다.
with tracer.start_as_current_span(
"db",
attributes={"db.system": "postgres"}
):
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchone()
이렇게 span을 덧씌우면 쿼리 실행 시간, 에러 여부, 예외 메시지 등이 모두 해당 span 안에서 자동으로 기록됩니다.
특히 쿼리가 느리게 수행되는 경우, 트레이스 뷰에서 DB 구간만 골라서 전체 요청 중 병목이 정확히 어디였는지 바로 확인할 수 있습니다.
서버 입장에서 보면 “어느 순간”이 아닌 “어느 코드 구간”에서 병목이 발생했는지가 명확히 보이기 때문에 운영 중 장애를 분석할 때도 굉장히 빠르게 판단할 수 있게 도와줍니다.
또 하나 중요한 포인트는, 가능하면 DB 호출 전체를 하나의 span으로 묶는 것이 좋다는 점입니다.
쿼리 builder, connection acquire, execute 등이 따로따로 나뉘어 있는 코드베이스도 많지만, 가능한 한 상위 구간에서 통째로 감싸야 추후 분석 시 단일 쿼리 단위로 보기가 편해집니다.
특히 여러 종류의 SQL 쿼리를 실행하는 경우에는 db.statement 속성에 SQL 텍스트 일부를 기록하는 것도 권장됩니다.
민감한 정보는 마스킹이 필요하지만, 최소한 “SELECT인지 UPDATE인지” 구분할 정도의 정보만 있어도 원인을 좁히는 속도가 확 올라가요.
💡 TIP: PostgreSQL 풀링 라이브러리(asyncpg.pool 등)를 사용할 때도, acquire() 직후가 아니라 실제 쿼리 실행 직전에 span을 여는 것이 훨씬 정확한 타이밍입니다.
요약하자면, tracer.start_as_current_span(“db”, attributes={“db.system”: “postgres”})는 PostgreSQL 호출을 정확히 기록하는 가장 단순하면서도 강력한 패턴입니다.
그리고 이 방식은 OTel 공식 스펙과도 일치하기 때문에, Jaeger·Tempo·Zipkin·Honeycomb·New Relic 등 어떤 observability 스택을 쓰더라도 정확히 PostgreSQL 호출로 인식하고 조회/필터링할 수 있습니다.
다음 단계에서는 PostgreSQL 전용 속성을 어떻게 더 세밀하게 다루는지 이어서 살펴보겠습니다.
🐘 PostgreSQL용 db.system postgres 속성 설계 포인트
OTel 트레이스를 PostgreSQL에 맞게 설계할 때 가장 먼저 떠올려야 하는 것은 db.system=”postgres” 속성입니다.
이 값은 단순한 텍스트가 아니라, OpenTelemetry 스펙에서 공식적으로 정의한 데이터베이스 시스템 식별자이기 때문에 관측 플랫폼에서 PostgreSQL 호출을 자동 인식하는 핵심 단서가 됩니다.
예를 들어 Tempo나 Jaeger 같은 뷰어에서는 db.system 값만으로도 PostgreSQL 호출만 모아서 필터링할 수 있고, 쿼리 지연을 특정 서비스나 특정 API로 역추적하는 작업이 훨씬 쉬워집니다.
PostgreSQL 스팬 설계에서 많이 놓치는 부분 중 하나는 추가 속성(attribute) 구성입니다.
기본적으로 db.system 외에도 다음 속성을 조합하면, 실무에서 훨씬 더 유용한 분석이 가능해집니다.
- 🛠️db.statement — SELECT, UPDATE, DELETE 등 쿼리의 목적 파악
- ⚙️db.operation — CRUD 유형을 명확히 남기기
- 🔌net.peer.name 또는 net.peer.port — 어떤 DB 인스턴스에 연결했는지 추적
특히 db.statement는 성능 분석 시 빛을 발합니다.
문자열 전체를 노출하기 어려운 서비스도 많지만, 최소한 “SELECT users FROM …” 정도만 남겨도 어떤 종류의 작업에서 지연이 생겼는지 훨씬 빠르게 판단할 수 있습니다.
또한 여러 SQL이 섞인 복잡한 비즈니스 로직이라면, 이 속성 덕분에 슬로우 쿼리 후보만 골라서 조회하는 필터링이 가능합니다.
아래는 PostgreSQL 호출에 속성을 조금 더 풍부하게 넣은 예시입니다.
보안 규정상 SQL 전체를 노출할 수 없다면 부분 마스킹을 적용하는 방식도 많이 사용됩니다.
with tracer.start_as_current_span(
"db",
attributes={
"db.system": "postgres",
"db.operation": "SELECT",
"db.statement": "SELECT * FROM users WHERE id = ?",
"net.peer.name": "primary-db",
"net.peer.port": 5432
}
):
do_query()
이런 방식으로 PostgreSQL 관련 attribute를 탄탄하게 구성해 두면, 단순히 “DB가 느린가 보다” 하는 수준을 넘어서, 어떤 테이블·어떤 쿼리 유형·어떤 DB 인스턴스에서 지연이 발생했는지까지 바로 파악할 수 있습니다.
특히 여러 마이크로서비스가 공용 DB를 사용하는 환경에서는 이런 속성이 장애 원인을 특정 서비스로 빠르게 좁히는 핵심 도구가 됩니다.
지금까지의 구성 요소들은 이후 HTTP 호출과 연결될 전체 트레이스 흐름의 기반이 되므로, 신경 써서 설계할수록 나중에 분석이 훨씬 편해진다는 점을 기억해 두면 좋습니다.
🔗 HTTP 클라이언트 트레이스와 헤더 전파 설정하기
데이터베이스 트레이싱만큼 중요한 것이 바로 서비스 간 요청을 이어 붙이는 HTTP 클라이언트 헤더 전파입니다.
외부 API나 내부 마이크로서비스로 요청이 나갈 때 traceparent·tracestate 헤더를 함께 넘기면, 다음 서비스에서도 동일한 트레이스 ID를 받아 이어서 span을 생성할 수 있습니다.
이 덕분에 한 사용자의 요청이 여러 서버를 거쳤음에도 전체 흐름을 하나의 트레이스 체인으로 재구성할 수 있어요.
OpenTelemetry 파이썬에서는 W3C Trace Context를 기본 포맷으로 사용하며, requests·httpx·aiohttp 등 다양한 HTTP 클라이언트에 적용할 수 있습니다.
핵심은 “현재 활성 span의 컨텍스트를 HTTP 헤더에 삽입한다”는 점으로, 대부분의 경우 아래 패턴만 따르면 간단하게 전파가 완성됩니다.
import requests
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
propagator = TraceContextTextMapPropagator()
def http_get(url):
headers = {}
propagator.inject(headers) # 현재 span의 컨텍스트를 헤더에 주입
return requests.get(url, headers=headers)
이 코드는 핵심만 담긴 최소 구성 예시입니다.
propagator.inject(headers)를 호출하면 현재 활성 span의 traceparent와 tracestate가 헤더에 자동으로 추가됩니다.
추가 설정 없이 이 헤더만 전파해도 다음 서비스에서는 해당 컨텍스트를 기반으로 새로운 span을 열고, 원래 요청과 동일한 트레이스에 자연스럽게 연결됩니다.
특히 API 체인이 길어질수록 이 방식은 장애 분석의 속도를 비약적으로 끌어올립니다.
실무에서 많이 쓰는 httpx나 aiohttp도 마찬가지 원리입니다.
예를 들어 httpx의 경우 request hook을 통해 전파할 수 있습니다.
import httpx
async def add_trace_headers(request):
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
propagator = TraceContextTextMapPropagator()
propagator.inject(request.headers)
async with httpx.AsyncClient(event_hooks={"request": [add_trace_headers]}) as client:
await client.get("https://example.com")
헤더 전파가 누락되면 문제는 생각보다 크게 나타납니다.
각 마이크로서비스가 자신의 span은 찍더라도, 서로 연결되지 않기 때문에 트레이스 뷰에는 서비스별 단편적인 정보만 보이게 됩니다.
이렇게 되면 “A 서비스에서 요청이 느려요”라고 할 때 실제 원인이 B나 C 서비스에 있을 가능성을 놓치게 됩니다.
즉, 헤더 전파는 트레이스의 품질을 좌우하는 가장 핵심 요소 중 하나라고 볼 수 있습니다.
⚠️ 주의: 역프록시(Nginx 등)가 traceparent 헤더를 덮어쓰거나 제거하는 설정을 가지고 있지는 않은지 반드시 확인해야 합니다.
정리하자면, tracer.start_as_current_span으로 내부 로직을 기록한 뒤, 서비스 밖으로 나가는 HTTP 요청에는 반드시 traceparent·tracestate 헤더를 inject해야 전체 체인이 완성됩니다.
이 구성이 갖춰져야 PostgreSQL과 외부 API 호출, 그리고 마이크로서비스 간 이동까지 모두 한눈에 이어진 트레이스 지도를 만들 수 있습니다.
🧪 전체 요청 흐름을 잇는 실전 파이썬 레시피
지금까지 살펴본 PostgreSQL 구간 트레이싱과 HTTP 헤더 전파를 하나의 흐름으로 묶으면, 단일 요청이 서비스 내부 로직과 외부 호출을 어떤 방식으로 타고 흐르는지 명확하게 재구성할 수 있습니다.
특히 웹 서버 → 비즈니스 로직 → DB → HTTP 클라이언트로 이어지는 흔한 패턴에서는 이 실전 레시피가 그대로 적용됩니다.
핵심은 루트 span을 만들고, 그 안에서 DB span과 HTTP span을 자연스럽게 연결하는 것입니다.
아래 예시는 FastAPI를 기준으로 구성했지만, Flask·Django 등 어떤 프레임워크든 동일한 흐름으로 적용할 수 있어요.
우선 요청당 하나의 루트 span을 만들고, 내부에서 DB와 외부 API 호출을 이어가는 구조를 보면 전체 트레이스가 어떻게 쌓이는지 감을 잡기 좋습니다.
from opentelemetry import trace
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
import requests
tracer = trace.get_tracer(__name__)
propagator = TraceContextTextMapPropagator()
def fetch_user(user_id):
# DB span
with tracer.start_as_current_span(
"db",
attributes={"db.system": "postgres", "db.operation": "SELECT"}
):
user = query_user(user_id)
# HTTP span + 헤더 전파
headers = {}
propagator.inject(headers)
with tracer.start_as_current_span("external_api_call"):
r = requests.get("https://api.example.com/profile", headers=headers)
return {"db": user, "api": r.json()}
위 코드에서 중요한 부분은 크게 세 가지입니다.
먼저, PostgreSQL 쿼리를 감싸는 start_as_current_span(“db”, {“db.system”: “postgres”})는 DB 병목을 식별하는 가장 기본 단위가 됩니다.
두 번째는 외부 API 호출을 하기 직전 propagator.inject(headers)를 실행해 현재 트레이스 컨텍스트를 전달하는 것입니다.
마지막으로 HTTP 호출 자체를 또 하나의 span으로 감싸면, 전체 트레이스 뷰에서 “입력 → DB → 외부 요청”이 직관적인 형태로 표시됩니다.
만약 서비스가 여러 단계의 마이크로서비스를 지나가야 한다면, 각 단계에서도 동일한 방식으로 헤더를 받아서 추출(extract)하고, 새로운 span을 연 뒤 다음 단계로 전파해 주면 됩니다.
이 과정을 거치면 단일 trace ID 아래에서 수십 개의 서비스가 연결된 체인 전체를 한 번에 시각화할 수 있습니다.
장애 분석 시 어느 지점에서 지연이 발생했는지 몇 초 만에 pinpoint할 수 있는 구조가 완성됩니다.
💎 핵심 포인트:
DB span, HTTP span, 헤더 전파는 따로 놓고 보면 단순해 보이지만, 실제 운영 환경에서는 이 세 가지가 완성되어야 진짜 “관측되는 서비스”가 구현됩니다.
여기까지 구성해 두면, 이후에는 애플리케이션 기능이 늘어나더라도 동일한 패턴으로 span을 쌓아 확장할 수 있습니다.
특히 데이터베이스와 외부 API 호출이 많은 서비스라면, 이 레시피 하나만으로도 성능 튜닝의 효율이 압도적으로 올라가는 것을 경험하게 됩니다.
❓ 자주 묻는 질문 (FAQ)
PostgreSQL span에 꼭 db.system postgres를 넣어야 하나요?
PostgreSQL은 “postgres”가 표준이며, 이를 설정해야 관측 도구에서 자동 필터링과 시각화가 제대로 동작합니다.
SQL 문장을 그대로 db.statement에 넣어도 괜찮나요?
최소한 SELECT인지 UPDATE인지 구분할 정도의 정보만 포함해도 충분히 분석에 도움이 됩니다.
HTTP 헤더 전파가 누락되면 어떤 문제가 생기나요?
이 경우 병목 지점을 찾기 어렵고, 전체 호출 흐름을 단일 트레이스 ID로 추적하는 기능을 잃게 됩니다.
requests 라이브러리에서 자동 전파는 불가능한가요?
오토-인스트루멘테이션을 사용할 수도 있지만 수동 방식이 더 명확하고 디버깅이 쉽습니다.
HTTP 클라이언트가 여러 개일 때도 같은 방식으로 전파하면 되나요?
구현 방식만 다르고 원리는 모두 같습니다.
DB 쿼리마다 span을 따로 생성하는 것이 좋나요?
다만 너무 많은 span을 생성하면 비용과 처리량에 영향을 줄 수 있어 적절한 균형이 필요합니다.
tracecontext 말고 b3 헤더도 전파해야 하나요?
만약 B3 기반 시스템과 연동해야 한다면 멀티 프로퍼게이터를 사용해 두 포맷을 함께 전파할 수 있습니다.
서비스가 많아지면 트레이스가 너무 복잡해지지 않나요?
필터 기능을 활용하면 특정 서비스나 구간만 선택적으로 분석할 수 있어 복잡도가 크게 문제 되지 않습니다.
💻 파이썬 OTel로 보는 PostgreSQL과 HTTP 요청 흐름
이 글에서는 파이썬에서 OpenTelemetry 트레이스를 구성할 때 꼭 챙겨야 할 두 가지 축, tracer.start_as_current_span(“db”, attributes={“db.system”:”postgres”})를 활용한 PostgreSQL 구간 계측과 HTTP 클라이언트 헤더 전파 방법을 함께 정리했습니다.
DB 쿼리 span에 db.system, db.statement 같은 속성을 알맞게 태깅하고, traceparent·tracestate를 HTTP 헤더에 실어 보내면 서비스 간 요청이 하나의 트레이스로 깔끔하게 이어집니다.
이 구조를 기반으로 마이크로서비스 환경에서도 느린 구간을 빠르게 찾아내고, 장애 원인을 정확히 좁혀 가는 실전 레시피까지 함께 확인할 수 있습니다.
🏷️ 관련 태그 : OpenTelemetry, 파이썬트레이싱, PostgreSQL성능, db.systempostgres, HTTP헤더전파, traceparent, 마이크로서비스관측, 파이썬OTel예제, 분산트레이싱, FastAPI모니터링