Langfuse는 자체적인 데이터 수집 프로토콜을 고집하는 대신, CNCF의 표준인 OpenTelemetry(OTel)를 기본 수집 파이프라인으로 채택했습니다. 물론 Python이나 JS/TS SDK를 사용하면 대부분이 추상화되어 크게 이해할 필요가 없지만, 작동 방식으로 조금 더 소상히 이해하거나 다른 언어(e.g. JAVA)를 사용하여 Langfuse를 제어하고자 하는 경우에는 OTel과 추상을 어떻게 맞대고 있는지 파악해야 합니다.
Langfuse에서 모든 관측 데이터의 컨테이너 역할을 하는 최상위 객체는 트레이스(Trace)입니다.
Langfuse JAVA SDK
찾아보다 보면 Langfuse JAVA SDK를 발견할 수 있습니다. 다만 현재의 Java SDK는 Python이나 JS/TS SDK처럼 트레이스, 스팬, 생성, 이벤트를 직접 다루는 고수준 관측 API라기보다 API client에 가깝습니다. 따라서 JAVA 기반 시스템에서 Langfuse로 관측을 보내려면 REST API를 직접 사용하거나, OTel 계측 후 Langfuse OTel 엔드포인트로 export하는 방식이 더 자연스럽습니다.
Trace이외에도 Langfuse문서에서 등장하는 트레이스, 스팬같은 용어가 등장합니다. 이것들은 Otel이 정한 데이터 구조(Protocol)입니다. Langfuse가 만든 말이 아닙니다. 단, 생성이나 이벤트는 Langfuse 용어입니다. Otel은 세상의 모든 작업을 그냥 스팬이라고 부릅니다.
트레이스
모든 관측 데이터의 컨테이너 역할을 하는 최상위 객체는 트레이스라고 했습니다. 이 또한 OTel과 Langfuse 공통입니다. 다만 기술적으로 트레이스는 루트 스팬의 다른 이름이 아니라, 동일한 trace id를 공유하고 parent-child 관계로 연결된 스팬들의 집합으로 이해하는 편이 더 정확합니다. 부모가 없는 첫 번째 스팬이 생성되면 새로운 trace id가 생기고, 그 스팬이 루트 스팬이 됩니다.
하지만 Langfuse는 LLM 앱을 분석하는 도구라서, OTel 스팬을 Langfuse에 더 잘 맞는 관측 유형으로 해석해 보여주고 싶어 합니다(ref1). 여기서부터 정말 헷갈립니다. 대표적으로 LLM 호출에 해당하는 스팬은 Langfuse에서 생성으로, 짧은 단발성 기록은 이벤트로 표시될 수 있습니다. Langfuse는 이러한 여러 유형을 통틀어 관측(Observation)이라는 상위 개념으로 다룹니다. 다만 이것이 OTel의 네이티브 타입 체계를 바꾼다는 뜻은 아닙니다. Langfuse는 OTel 데이터를 받아 자체적인 observation type으로 해석해 보여준다고 이해하는 편이 더 정확합니다.
이벤트
OTel에서 말하는 이벤트와 Langfuse에서 말하는 이벤트는 동일한 개념이 아닙니다. 정확히 말하면, OTel에는 적어도 두 가지 맥락이 있습니다. 첫째는 span.add_event(...)로 추가하는 스팬 이벤트이고(ref4), 둘째는 LogRecord 기반의 로그 신호입니다(ref2). LogRecord는 Loki 같은 로그 전용 시스템으로 데이터를 보낼 때의 맥락에서 자주 등장합니다(ref3).
반면 Langfuse의 이벤트는 Langfuse가 정의한 observation type입니다. 즉, OTel의 스팬 이벤트와 1:1로 대응하는 클래스가 아닙니다. OTel 표준 스팬 이벤트를 추가하려면 span.add_event("new event")를 사용합니다. Langfuse에서 이벤트 관측으로 보이게 하려면 langfuse.observation.type을 event로 명시하거나 Langfuse SDK의 event API를 사용한다고 이해하는 편이 정확합니다.
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
# 아래 예시는 순수 OTel tracer를 사용할 때의 형태입니다.
# OTel tracer의 표준 컨텍스트 매니저는 start_as_current_span()입니다.
with tracer.start_as_current_span("chat-message"):
with tracer.start_as_current_span("retrieval"):
with tracer.start_as_current_span("vector_store"):
with tracer.start_as_current_span("context-encoding"):
...
with tracer.start_as_current_span("llm-response") as span:
span.set_attribute("langfuse.observation.type", "generation")
...
with tracer.start_as_current_span("postprocessing"):
...
with tracer.start_as_current_span("response-sent") as span:
span.set_attribute("langfuse.observation.type", "event")
...
Python
복사
파이썬 의사 코드: 이런 구조로 쌓으면 OTel로 그림의 구조를 만들 수 있습니다.
Langfuse SDK의 observation helper를 사용할 때는 이와 다른 메서드 이름이 등장할 수 있지만, 위 코드는 OTel 표준 예시로 읽는 편이 맞습니다.
OTel 표준으로 Langfuse로 트레이스를 보낼 때, Langfuse에서 사용자·세션 단위의 집계와 필터링이 잘 되려면 trace-level 속성이 관련 스팬들에 일관되게 전파되는 것이 중요합니다. v3 기준으로 대표적으로 떠올리면 사용자 ID(user.id), 세션 ID(session.id), 메타데이터(langfuse.trace.metadata), 태그(langfuse.trace.tags) 같은 속성이 있습니다. 이를 수동으로 각 스팬에 반복해서 넣기보다 컨텍스트 전파 방식으로 유지하는 편이 자연스럽습니다.
user.id와 session.id
현재 v3 문서를 기준으로 설명할 때는 user.id, session.id, langfuse.trace.metadata, langfuse.trace.tags를 대표 표기로 삼는 편이 가장 안전합니다. 예전 표기와 혼용되는 사례가 있더라도, 새 글에서는 현재 문서 기준의 canonical key를 중심으로 적는 편이 독자에게 덜 혼란스럽습니다.
이러한 속성을 전파할 때는 OpenTelemetry Baggage 같은 컨텍스트 전파 메커니즘을 사용할 수 있습니다. Baggage는 지정된 키-값 쌍을 컨텍스트에 담아 하위 스팬들로 전달하는 데 유용합니다. 한편 Langfuse v3 SDK를 직접 쓰는 경우에는 propagate_attributes() 같은 helper를 활용하는 방식이 더 자연스럽습니다.
스팬을 Langfuse상에서 LLM 관련 생성 작업으로 더 잘 해석하게 하려면 Langfuse가 인식할 수 있는 속성(Attribute)을 스팬에 포함시켜야 합니다.
gen_ai.*는 OTel의 GenAI semantic conventions와 연결되는 규격입니다. Langfuse는 이를 지원하므로 gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens 같은 속성을 이해할 수 있습니다. 또한 generation 분류를 확실히 하고 싶다면 관련 gen_ai.* 속성과 함께 langfuse.observation.type=generation을 명시하는 방식을 함께 떠올리면 됩니다.
import json
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("chat-message") as root_span:
root_span.set_attribute("user.id", "q2kj3b1kjggsaapvskjkn")
root_span.set_attribute("session.id", "session_abc123")
with tracer.start_as_current_span("llm-response") as span:
span.set_attribute("langfuse.observation.type", "generation")
span.set_attribute("gen_ai.request.model", "gpt-4o")
span.set_attribute("gen_ai.input.messages", json.dumps(messages, ensure_ascii=False))
response = client.chat.completions.create(model=model_name, messages=messages)
answer_text = response.choices[0].message.content
span.set_attribute("gen_ai.output.messages", json.dumps([
{"role": "assistant", "content": answer_text}
], ensure_ascii=False))
span.set_attribute("gen_ai.usage.input_tokens", response.usage.prompt_tokens)
span.set_attribute("gen_ai.usage.output_tokens", response.usage.completion_tokens)
Python
복사
위 예시는 OTel 속성만으로 Langfuse가 이해할 수 있는 형태를 만드는 쪽에 가깝습니다. Langfuse SDK를 직접 쓰면 generation 생성, 입력/출력 기록, score 연결 같은 작업을 더 높은 수준의 API로 다룰 수 있습니다.
관측 타입 명시 vs 자동 추론
gen_ai.* 속성이 있으면 Langfuse가 해당 스팬을 LLM 관련 작업으로 해석할 여지가 커집니다. 다만 글을 읽는 사람이 혼동하지 않게 하려면, 자동 추론에만 기대기보다 langfuse.observation.type을 generation 또는 event로 명시하는 예시를 함께 보여 주는 편이 더 안전합니다(ref5).
JAVA 기반의 시스템에서 OTel을 이용하여 Langfuse에 관측을 등록하는 방법은 위 배경 지식을 바탕으로 Langfuse의 통합 가이드 Integrating Langfuse with Spring AI을 참고하세요. 이 문서는 Spring AI(AI 호출에 대한 OTel 트레이싱 기능이 내장되어 있음) 프레임워크를 사용하여 Langfuse에 관측을 등록하는 방법을 설명합니다. 이 문서는 JAVA/OTel → Langfuse를 구현을 간단히 확인하는 데 초점을 맞추므로, 관측이 중첩된 복잡한 집계가 필요하다면 앞서 잠깐 언급했던 OTel Baggage가 필요할 수 있음을 떠올려볼 수 있습니다.
이렇게 Langfuse 시스템에 관측이 등록되었다면, REST API나 SDK를 이용하여 ‘어떤 관측이 몇 점’이라고 점수를 매기는 일(ref6)을 비롯한 다양한 작업들을 수행할 수 있습니다. 예를 들어 관측에 점수를 매기는 구체적인 방법은 Scores via API/SDK 문서를 참고하세요. 이 문서에서 다루는 API 호출에 필요한 호스트, 공개키, 비밀키는 Langfuse의 Project Settings에서 API Keys를 생성할 때 얻을 수 있습니다.
예를 들어 thumbs up/down 같은 아주 단순한 사용자 피드백도 score로 바로 기록할 수 있습니다.
from langfuse import get_client
langfuse = get_client()
# observation(예: 특정 generation)에 대한 thumbs up
langfuse.create_score(
trace_id=trace_id,
observation_id=observation_id,
name="helpfulness",
value=1,
data_type="BOOLEAN",
comment="thumbs up",
)
# thumbs down이면 value=0으로 기록하면 됩니다.
Python
복사
특정 generation이나 event에 대한 피드백이면 observation_id를 함께 넣고, 대화 전체(trace)에 대한 피드백이면 observation_id 없이 trace_id만 사용하면 됩니다.
parse me : 언젠가 이 메모에 쓰이면 좋을 것 같은 재료들입니다.
1.
None
from : 이 메모에 쓰인 생각을 만든 과거의 생각들과 연관관계를 설명합니다.
1.
•
앞의 글은 OTel이 어떤 문제를 해결하는지 설명한다. 이 글에 나타난 Langfuse의 ‘관측’은 LLM에 있어 트레이싱을 구체화하는 과정에 생겨난 개념이다. 본문에 “Langfuse는 트레이스 중심의 도구이기 때문에 두 번째 개념은 고려할 필요가 없습니다.”라고 언급한 것도 그러한 맥락에서 이해할 수 있다.
supplementary : 이 메모에 작성된 생각을 뒷받침하는 새로운 메모입니다.
1.
None
opposite : 이 메모에 작성된 생각과 대조되는 새로운 메모입니다.
1.
None
to : 이 메모에 작성된 생각으로부터 발전된 생각의 메모입니다.
1.
ref : 생각에 참고한 자료입니다.
프로젝트메모 템플릿 버전 2025.11.16

