feat(observability): Langfuse 전송 — 턴 마무리(사용자 trace)·게이트웨이(FabriX generation) 두 훅, SDK 없이 HTTP. deploy/langfuse 에 compose+env+반입 절차

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
lee-hyeon-cheol
2026-09-22 13:44:12 +09:00
co-authored by Claude Fable 5.1
parent 95b169a7b9
commit 0321050053
13 changed files with 617 additions and 0 deletions
+21
View File
@@ -20,12 +20,14 @@ import asyncio
import json
import logging
import time
from datetime import datetime, timezone
from django.conf import settings
from django.http import StreamingHttpResponse
from django.views.decorators.csrf import csrf_exempt
from apps.accounts.authentication import user_from_token
from apps.gateway import langfuse
from asgiref.sync import sync_to_async
from common.envelope import json_error
from common.opencode_service import opencode_service
@@ -248,6 +250,25 @@ async def _finalize(session: ChatSession, state: TurnState, started: float, *, f
if title:
session.title_llm = title[:200]
await session.asave(update_fields=["is_generating", "title_llm", "updated_at"])
# 관측: 사용자 단위 trace 하나 + 답변 generation 하나. 실패해도 여기까진 이미 저장됨.
if langfuse.enabled():
tid = f"{session.id}-{int(started * 1000)}"
user_email = await sync_to_async(lambda: session.user.email)()
meta = {"failed": failed} if failed else {}
langfuse.send_later([
langfuse.trace(tid, "chat", userId=user_email, sessionId=session.id, input=state.user_text,
output=content, metadata=meta, tags=["codeassist"]),
langfuse.generation(
tid, "opencode-turn",
startTime=datetime.fromtimestamp(started, timezone.utc).isoformat(),
endTime=langfuse.now_iso(),
input=state.user_text, output=content,
usage=langfuse.usage_of(usage["input"], usage["output"]) if usage else None,
level="ERROR" if failed else "DEFAULT", statusMessage=failed or "",
metadata={"elapsed_ms": usage["elapsed_ms"]} if usage else {},
),
])
return usage, title