dify/api/core/ops/unified_trace/entities.py
Blackoutta 3a8fbcbd02
feat(trace): add provider-neutral unified tracing (#39451)
Co-authored-by: EvoX <evox@evomap.ai>
2026-08-21 11:28:18 +00:00

80 lines
2.4 KiB
Python

"""Provider-independent trace entities emitted by the unified trace builder."""
from datetime import datetime
from enum import StrEnum
from typing import Any, Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
from core.helper.trace_id_helper import ParentTraceContext
class CanonicalSpanKind(StrEnum):
CHAIN = "chain"
LLM = "llm"
RETRIEVER = "retriever"
TOOL = "tool"
AGENT = "agent"
class CanonicalSpanStatus(StrEnum):
OK = "ok"
ERROR = "error"
class CanonicalSpan(BaseModel):
"""One provider-neutral operation with an explicit parent."""
id: str
parent_id: str | None
name: str
kind: CanonicalSpanKind
start_time: datetime
end_time: datetime | None
inputs: Any = None
outputs: Any = None
status: CanonicalSpanStatus
error: str | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
synthetic: bool = False
can_parent_workflow: bool = False
publishes_parent_context: bool = False
links: tuple[str, ...] = ()
model_config = ConfigDict(extra="forbid", frozen=True)
class CanonicalTrace(BaseModel):
"""A parent-before-child span tree ready for a provider adapter."""
trace_id: str
session_id: str
root_span_id: str
spans: tuple[CanonicalSpan, ...]
external_parent: ParentTraceContext | None = None
required_parent_context_id: str | None = None
model_config = ConfigDict(extra="forbid", frozen=True)
@model_validator(mode="after")
def validate_fragment(self) -> Self:
if self.external_parent is not None and self.required_parent_context_id is not None:
raise ValueError("canonical trace cannot require two external parent modes")
seen: set[str] = set()
root_seen = False
for span in self.spans:
if span.id in seen:
raise ValueError(f"duplicate canonical span id: {span.id}")
if span.id == self.root_span_id:
root_seen = True
if span.parent_id is not None:
raise ValueError("canonical trace root cannot have a local parent")
elif span.parent_id is None or span.parent_id not in seen:
raise ValueError(f"canonical span parent must appear first: {span.id}")
seen.add(span.id)
if not root_seen:
raise ValueError("canonical trace root is missing")
return self