Langfuse SDK 계측 (Instrumentation)
Langfuse SDK 계측 (Instrumentation)
Langfuse SDK로 애플리케이션을 계측하는 방법에는 크게 두 갈래가 있어요. OpenAI·LangChain·Vercel AI SDK 같은 인기 LLM·에이전트 라이브러리용 네이티브 통합을 쓰면 관찰·트레이스가 자동으로 생기고 프롬프트·응답·사용량·오류까지 잡아줘요. 아니면 SDK로 직접 계측할 수도 있는데, 관찰을 만드는 방식이 컨텍스트 매니저·observe 래퍼·수동 관찰 세 가지로 나뉘어요. 셋은 서로 섞어 쓸 수 있어서, 데코레이터로 만든 관찰을 컨텍스트 매니저 안에 중첩하거나 네이티브 통합과 수동 스팬을 함께 쓸 수 있어요.
컨텍스트 매니저 (Context manager)
컨텍스트 매니저는 새 스팬을 만들고, 그 기간 동안 OTel 컨텍스트의 활성 관찰로 설정해요. 이 블록 안에서 새로 만드는 관찰은 자동으로 그 스팬의 자식이 돼요. Python SDK의 start_as_current_observation()이 대표적이고, with 블록 안의 자식 관찰은 부모를 자동으로 물려받아요.
from langfuse import get_client, propagate_attributes
langfuse = get_client()
with langfuse.start_as_current_observation(
as_type="span",
name="user-request-pipeline",
input={"user_query": "Tell me a joke"},
) as root_span:
with propagate_attributes(user_id="user_123", session_id="session_abc"):
with langfuse.start_as_current_observation(
as_type="generation",
name="joke-generation",
model="gpt-4o",
) as generation:
generation.update(output="Why did the span cross the road?")
root_span.update(output={"final_joke": "..."})
관찰의 타입은 as_type 파라미터로 지정할 수 있어요. JS/TS 쪽은 startActiveObservation이 콜백을 받아, 심지어 async 경계를 넘어서도 새 스팬을 활성으로 만들고 끝에서 자동으로 종료해요.
observe 래퍼
observe 데코레이터는 함수 내부 로직을 수정하지 않고도 입력·출력·타이밍·오류를 자동으로 캡처하는 쉬운 방법이에요.
from langfuse import observe
@observe()
def my_data_processing_function(data, parameter):
return {"processed_data": data, "status": "ok"}
@observe(name="llm-call", as_type="generation")
async def my_async_llm_call(prompt_text):
return "LLM response"
큰 입력/출력을 캡처하면 오버헤드가 붙을 수 있어요. 데코레이터마다 capture_input=False, capture_output=False로 끄거나 LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED 환경변수로 조절할 수 있어요.
수동 관찰 (Manual observations)
수동으로 관찰을 만들 수도 있어요. 주 실행 흐름과 나란히 돌지만 같은 트레이스의 일부여야 하는 백그라운드 작업, 시작·끝이 연속되지 않는 이벤트로 결정되는 관찰, 특정 컨텍스트 블록에 묶이기 전에 관찰 객체 참조를 얻어야 하는 경우에 유용해요.
from langfuse import get_client
langfuse = get_client()
span = langfuse.start_observation(name="manual-span")
span.update(input="Data for side task")
child = span.start_observation(name="child-span", as_type="generation")
child.end()
span.end()
수동 관찰은 start_as_current_* 계열과 달리 활성 컨텍스트를 바꾸지 않아요. 즉 이전에 활성이던 스팬이 계속 현재 컨텍스트로 남아요. 또 with 블록으로 관리되지 않으므로 반드시 .end()를 직접 호출해야 해요. 잊으면 Langfuse에 관찰이 불완전하게 남거나 누락될 수 있어요. start_as_current_*를 with와 함께 쓰면 이 과정을 알아서 처리해줘요.
관찰 중첩
Langfuse SDK 메서드는 관찰 중첩을 자동으로 처리해요. observe 래퍼를 쓰면 함수 호출 계층이 그대로 트레이스에 반영되고, 컨텍스트 매니저는 OpenTelemetry의 컨텍스트 전파가 처리를 담당해요. 새 관찰은 생성 당시 컨텍스트에서 활성이던 관찰의 자식이 돼요. 수동 관찰의 자식은 부모 LangfuseSpan/LangfuseGeneration 객체의 메서드로 만들어야 하고, 이 자식들은 _as_current_ 변형을 쓰지 않는 한 활성 컨텍스트가 되지 않아요.
관찰 업데이트
코드가 실행되면서 관찰에 새 정보를 업데이트할 수 있어요. 변수에 할당된 관찰은 객체의 .update() 메서드를 쓰고, 컨텍스트에서 현재 활성인 관찰을 직접 참조 없이 바꾸려면 Python SDK의 langfuse.update_current_span()·langfuse.update_current_generation()을 써요.
from langfuse import get_client
langfuse = get_client()
with langfuse.start_as_current_observation(as_type="generation", name="llm-call", model="gpt-5-mini") as gen:
gen.update(input={"prompt": "Why is the sky blue?"})
response_text = "Rayleigh scattering..."
gen.update(
output=response_text,
usage_details={"input_tokens": 5, "output_tokens": 50},
metadata={"confidence": 0.9}
)
관찰에 속성 추가
userId, sessionId, metadata, version, tags, environment(Python SDK), traceName 속성을 관찰에 추가할 수 있어요. Python SDK에서는 propagate_attributes()를 써서 속성을 관찰에 붙여요.
from langfuse import get_client, propagate_attributes
langfuse = get_client()
with langfuse.start_as_current_observation(as_type="span", name="user-workflow"):
with propagate_attributes(
user_id="user_123",
session_id="session_abc",
metadata={"experiment": "variant_a"},
version="1.0",
environment="staging",
trace_name="user-workflow",
):
with langfuse.start_as_current_observation(as_type="generation", name="llm-call"):
pass
여러 서비스에 걸친 분산 트레이싱이 필요하면 as_baggage 파라미터로 HTTP 헤더를 통해 속성을 전파해요. 다만 baggage 전파를 켜면 속성이 모든 아웃바운드 HTTP 헤더에 들어가므로, 분산 트레이싱에 필요한 민감하지 않은 값만 써야 해요.
클라이언트 수명주기와 플러시
Langfuse SDK는 비동기라서 스팬을 백그라운드에 버퍼링해요. 스크립트·서버리스 함수·워커 같은 단명 프로세스에서는 데이터 손실을 막으려면 항상 flush()나 shutdown()을 호출해야 해요. flush()는 버퍼된 모든 관찰을 Langfuse API로 보내도록 즉시 트리거하고, shutdown()은 버퍼 플러시 후 백그라운드 스레드를 깔끔히 종료해요. SDK는 정상 종료 시 shutdown()을 부르는 atexit 훅을 등록하지만, 신호를 받는 장기 실행 데몬이나 atexit가 확실히 안 도는 환경(특정 서버리스·강제 종료)에서는 직접 호출을 권장해요.