fix(observability): Phoenix 는 OTLP protobuf 만 받음(JSON 415) — opentelemetry-proto 로 직렬화, 로컬 Phoenix 실측 통과
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e60c5bc273
commit
68b55907f0
@@ -3,6 +3,7 @@
|
||||
대상은 설정으로 고름(둘 다 켜도 됨):
|
||||
LANGFUSE_HOST → {host}/api/public/otel/v1/traces (Basic 인증, langfuse.* 속성)
|
||||
PHOENIX_HOST → {host}/v1/traces (인증 없음, OpenInference 속성: openinference.span.kind, input.value …)
|
||||
Phoenix 는 JSON 을 안 받아서(415, 2026-09-22 실측) protobuf 로 바꿔 보냄 — opentelemetry-proto 필요
|
||||
속성은 두 벌을 같은 span 에 같이 실음 — 각자 자기 것만 읽고 나머진 metadata 로 떨어짐.
|
||||
|
||||
v4 는 옛 /api/public/ingestion 이 막혀서(score 만) OTel 엔드포인트로 감. 트레이스 하나 = 루트 span(trace 속성) + 자식 span(generation).
|
||||
@@ -186,6 +187,24 @@ def usage_of(inp: int | None, out: int | None) -> dict | None:
|
||||
return {"input": inp or 0, "output": out or 0, "total": (inp or 0) + (out or 0)}
|
||||
|
||||
|
||||
def _to_protobuf(body: dict) -> bytes:
|
||||
"""OTLP JSON → protobuf 바이트. traceId/spanId 는 JSON 이 hex, proto-JSON 은 base64 라 바꿔 넣음."""
|
||||
import base64
|
||||
import copy
|
||||
|
||||
from google.protobuf.json_format import ParseDict
|
||||
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest
|
||||
|
||||
b = copy.deepcopy(body)
|
||||
for rs in b["resourceSpans"]:
|
||||
for ss in rs["scopeSpans"]:
|
||||
for sp in ss["spans"]:
|
||||
for k in ("traceId", "spanId", "parentSpanId"):
|
||||
if k in sp:
|
||||
sp[k] = base64.b64encode(bytes.fromhex(sp[k])).decode()
|
||||
return ParseDict(b, ExportTraceServiceRequest()).SerializeToString()
|
||||
|
||||
|
||||
def _otlp_body(spans: list[dict]) -> dict:
|
||||
project = settings.LANGFUSE.get("project") or "codeassist"
|
||||
return {"resourceSpans": [{
|
||||
@@ -200,17 +219,21 @@ async def send(spans: list[dict]) -> bool:
|
||||
if not spans or not enabled():
|
||||
return False
|
||||
body = _otlp_body(spans)
|
||||
targets: list[tuple[str, str, dict, tuple | None]] = []
|
||||
# (이름, url, headers, auth, protobuf 여부)
|
||||
targets: list[tuple[str, str, dict, tuple | None, bool]] = []
|
||||
if cfg.get("host"):
|
||||
targets.append(("langfuse", cfg["host"].rstrip("/") + "/api/public/otel/v1/traces",
|
||||
{"x-langfuse-ingestion-version": "4"}, (cfg.get("public_key", ""), cfg.get("secret_key", ""))))
|
||||
{"x-langfuse-ingestion-version": "4"}, (cfg.get("public_key", ""), cfg.get("secret_key", "")), False))
|
||||
if cfg.get("phoenix"):
|
||||
targets.append(("phoenix", cfg["phoenix"].rstrip("/") + "/v1/traces", {}, None))
|
||||
targets.append(("phoenix", cfg["phoenix"].rstrip("/") + "/v1/traces", {"Content-Type": "application/x-protobuf"}, None, True))
|
||||
ok = False
|
||||
for name, url, headers, auth in targets:
|
||||
for name, url, headers, auth, pb in targets:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0, transport=TRANSPORT) as client:
|
||||
resp = await client.post(url, json=body, headers=headers, auth=auth)
|
||||
if pb:
|
||||
resp = await client.post(url, content=_to_protobuf(body), headers=headers, auth=auth)
|
||||
else:
|
||||
resp = await client.post(url, json=body, headers=headers, auth=auth)
|
||||
if resp.status_code != 200:
|
||||
log.warning("%s ← %s %s", name, resp.status_code, resp.text[:200])
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user