fix(langfuse): v4 가 옛 ingestion 을 거부 — OTLP/HTTP JSON(/api/public/otel/v1/traces)으로 전환, 로컬 Langfuse 실측 통과. 사용자·세션 속성은 자식 span 에도 복사
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0321050053
commit
55f2e8fb60
@@ -1,4 +1,4 @@
|
||||
"""Langfuse 전송 — 이벤트 모양·인증·게이트웨이/턴 마무리 훅. 서버는 MockTransport."""
|
||||
"""Langfuse 전송(OTLP/HTTP JSON) — span 모양·인증·게이트웨이/턴 마무리 훅. 서버는 MockTransport."""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
@@ -19,27 +19,38 @@ def lf_server(monkeypatch):
|
||||
|
||||
def handler(req: httpx.Request) -> httpx.Response:
|
||||
got.append({"url": str(req.url), "auth": req.headers.get("authorization"), "body": json.loads(req.content)})
|
||||
return httpx.Response(207, json={"successes": [], "errors": []})
|
||||
return httpx.Response(200, json={})
|
||||
|
||||
monkeypatch.setattr(langfuse, "TRANSPORT", httpx.MockTransport(handler))
|
||||
return got
|
||||
|
||||
|
||||
def _attrs(span: dict) -> dict:
|
||||
out = {}
|
||||
for a in span["attributes"]:
|
||||
v = a["value"]
|
||||
out[a["key"]] = v.get("stringValue", v.get("intValue", v.get("boolValue")))
|
||||
return out
|
||||
|
||||
|
||||
@override_settings(LANGFUSE=LF)
|
||||
async def test_send_batch_shape_and_basic_auth(lf_server):
|
||||
ok = await langfuse.send([
|
||||
langfuse.trace("t1", "chat", userId="u@x.com", sessionId="s1"),
|
||||
langfuse.generation("t1", "gen", model="581", usage=langfuse.usage_of(10, 5)),
|
||||
])
|
||||
assert ok and len(lf_server) == 1
|
||||
async def test_send_otlp_shape_and_basic_auth(lf_server):
|
||||
root = langfuse.trace("t1", "chat", userId="u@x.com", sessionId="s1", tags=["x"])
|
||||
gen = langfuse.generation("t1", "gen", model="581", usage=langfuse.usage_of(10, 5), parent=root)
|
||||
assert await langfuse.send([root, gen]) and len(lf_server) == 1
|
||||
req = lf_server[0]
|
||||
assert req["url"] == "http://lf.test/api/public/ingestion"
|
||||
assert req["url"] == "http://lf.test/api/public/otel/v1/traces"
|
||||
assert req["auth"] == "Basic " + base64.b64encode(b"pk-lf-a:sk-lf-b").decode()
|
||||
batch = req["body"]["batch"]
|
||||
assert [e["type"] for e in batch] == ["trace-create", "generation-create"]
|
||||
assert batch[0]["body"] == {"id": "t1", "name": "chat", "userId": "u@x.com", "sessionId": "s1"}
|
||||
assert batch[1]["body"]["traceId"] == "t1" and batch[1]["body"]["usage"] == {"input": 10, "output": 5, "total": 15}
|
||||
assert batch[0]["id"] != batch[1]["id"] and batch[0]["timestamp"].endswith("Z")
|
||||
spans = req["body"]["resourceSpans"][0]["scopeSpans"][0]["spans"]
|
||||
assert len(spans) == 2 and spans[0]["traceId"] == spans[1]["traceId"] and len(spans[0]["traceId"]) == 32
|
||||
assert spans[1]["parentSpanId"] == spans[0]["spanId"] and "parentSpanId" not in spans[0]
|
||||
ra, ga = _attrs(spans[0]), _attrs(spans[1])
|
||||
assert ra["langfuse.trace.name"] == "chat" and ra["langfuse.user.id"] == "u@x.com" and ra["langfuse.session.id"] == "s1"
|
||||
assert json.loads(ra["langfuse.trace.tags"]) == ["x"]
|
||||
assert ga["langfuse.observation.type"] == "generation" and ga["langfuse.observation.model.name"] == "581"
|
||||
assert ga["langfuse.user.id"] == "u@x.com" and ga["langfuse.session.id"] == "s1" # 자식에도 복사
|
||||
assert json.loads(ga["langfuse.observation.usage_details"]) == {"input": 10, "output": 5, "total": 15}
|
||||
assert int(spans[1]["endTimeUnixNano"]) >= int(spans[1]["startTimeUnixNano"])
|
||||
|
||||
|
||||
@override_settings(LANGFUSE={"host": "", "public_key": "", "secret_key": ""})
|
||||
@@ -80,8 +91,11 @@ async def test_finalize_sends_user_trace(lf_server, django_user_model, monkeypat
|
||||
await _finalize(session, TurnState("ses_1", "질문"), 1.0)
|
||||
await asyncio.gather(*list(langfuse._pending))
|
||||
assert len(lf_server) == 1
|
||||
batch = lf_server[0]["body"]["batch"]
|
||||
t, g = batch[0]["body"], batch[1]["body"]
|
||||
assert t["userId"] == "u@x.com" and t["sessionId"] == "ses_1" and t["input"] == "질문" and t["output"] == "답"
|
||||
assert g["traceId"] == t["id"] and g["usage"] == {"input": 3, "output": 5, "total": 8} and g["level"] == "DEFAULT"
|
||||
spans = lf_server[0]["body"]["resourceSpans"][0]["scopeSpans"][0]["spans"]
|
||||
t, g = _attrs(spans[0]), _attrs(spans[1])
|
||||
assert t["langfuse.user.id"] == "u@x.com" and t["langfuse.session.id"] == "ses_1"
|
||||
assert t["langfuse.trace.input"] == "질문" and t["langfuse.trace.output"] == "답"
|
||||
assert spans[1]["parentSpanId"] == spans[0]["spanId"]
|
||||
assert json.loads(g["langfuse.observation.usage_details"]) == {"input": 3, "output": 5, "total": 8}
|
||||
assert g["langfuse.observation.level"] == "DEFAULT" and spans[1]["status"]["code"] == 1
|
||||
assert stream_mod is not None
|
||||
|
||||
Reference in New Issue
Block a user