|
33 | 33 |
|
34 | 34 | from __future__ import annotations |
35 | 35 |
|
| 36 | +import hashlib |
36 | 37 | import json |
37 | 38 | import uuid as _uuid |
38 | 39 | from contextlib import ExitStack |
|
53 | 54 | ) from exc |
54 | 55 |
|
55 | 56 |
|
56 | | -def _to_otel_trace_id(trace_id: str) -> str: |
57 | | - """Convert OA's UUID4-formatted invocation_id to OTel's 32-char |
58 | | - hex trace_id form (no dashes). |
59 | | -
|
60 | | - Langfuse v4 is OTel-based: trace IDs are 128-bit integers |
61 | | - serialized as 32 lowercase hex characters. OA's invocation_id is |
62 | | - a standard UUID4 (8-4-4-4-12 dashed hex); same 128 bits, different |
63 | | - representation. Passing the dashed form to Langfuse v4 fails with |
64 | | - ``int(..., 16)`` parsing in the SDK's internals. |
65 | | -
|
66 | | - Non-UUID inputs pass through unchanged so adapter consumers can |
67 | | - pass an already-OTel-formatted trace_id if they have one. |
68 | | -
|
69 | | - Trade-off: the spec §8.4.1 "trace.id MUST equal invocation_id |
70 | | - verbatim" contract is met content-wise (same 128 bits) but not |
71 | | - representation-wise. Users querying Langfuse for an OA |
72 | | - invocation_id need to strip dashes before searching. Documented |
73 | | - in the adapter's class docstring. |
74 | | - """ |
| 57 | +def _is_uuid(value: str) -> bool: |
75 | 58 | try: |
76 | | - return _uuid.UUID(trace_id).hex |
| 59 | + _uuid.UUID(value) |
77 | 60 | except (ValueError, AttributeError): |
78 | | - return trace_id |
| 61 | + return False |
| 62 | + return True |
| 63 | + |
| 64 | + |
| 65 | +# Per observability §8.4.1: Langfuse v4 trace ids are 128-bit values |
| 66 | +# rendered as 32 lowercase hex. A UUID invocation_id maps to its hex |
| 67 | +# form (dashes stripped). A non-UUID maps to the first 16 bytes of |
| 68 | +# SHA-256(invocation_id) as 32 hex (the same derivation as Langfuse's |
| 69 | +# create_trace_id(seed), so a consumer can reproduce it); the raw id is |
| 70 | +# also written to trace.metadata.invocation_id (see `trace`) for lookup. |
| 71 | +def _to_otel_trace_id(trace_id: str) -> str: |
| 72 | + """Return the 32-char hex Langfuse trace id for an OA invocation_id.""" |
| 73 | + if _is_uuid(trace_id): |
| 74 | + return _uuid.UUID(trace_id).hex |
| 75 | + return hashlib.sha256(trace_id.encode("utf-8")).digest()[:16].hex() |
| 76 | + |
| 77 | + |
| 78 | +def langfuse_trace_id(invocation_id: str) -> str: |
| 79 | + """Return the Langfuse ``trace.id`` for an OA ``invocation_id``. |
| 80 | +
|
| 81 | + Public helper for mapping a logged ``invocation_id`` (a dashed UUID |
| 82 | + or a caller-supplied non-UUID string) to the 32-char hex |
| 83 | + ``trace.id`` Langfuse stores, e.g. to build a direct trace URL. |
| 84 | + """ |
| 85 | + return _to_otel_trace_id(invocation_id) |
79 | 86 |
|
80 | 87 |
|
81 | 88 | def _stringify_metadata(metadata: dict[str, Any] | None) -> dict[str, str]: |
@@ -231,10 +238,14 @@ def trace( |
231 | 238 | # it via propagate_attributes on every observation under this |
232 | 239 | # trace_id so the trace's display name + metadata stay |
233 | 240 | # consistent under v4's last-wins semantics. |
234 | | - self._trace_info[id] = { |
235 | | - "name": name, |
236 | | - "metadata": dict(metadata) if metadata is not None else {}, |
237 | | - } |
| 241 | + md: dict[str, Any] = dict(metadata) if metadata is not None else {} |
| 242 | + # Non-UUID invocation_id: the derived trace.id is a hash, not |
| 243 | + # reversible to the caller's id, so surface the raw id under |
| 244 | + # trace.metadata.invocation_id for lookup (§8.4.1). The key is |
| 245 | + # reserved (proposal 0041), so no caller metadata collides. |
| 246 | + if not _is_uuid(id): |
| 247 | + md.setdefault("invocation_id", id) |
| 248 | + self._trace_info[id] = {"name": name, "metadata": md} |
238 | 249 |
|
239 | 250 | def update_trace( |
240 | 251 | self, |
|
0 commit comments